├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── email.android.js
├── email.ios.js
├── index.d.ts
├── package.json
└── references.d.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | node_modules/
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | .idea
2 | .vscode
3 | references.d.ts
4 | README.md
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NativeScript Email
2 |
3 | [![NPM version][npm-image]][npm-url]
4 | [![Downloads][downloads-image]][npm-url]
5 | [![Twitter Follow][twitter-image]][twitter-url]
6 |
7 | [npm-image]:http://img.shields.io/npm/v/nativescript-email.svg
8 | [npm-url]:https://npmjs.org/package/nativescript-email
9 | [downloads-image]:http://img.shields.io/npm/dm/nativescript-email.svg
10 | [twitter-image]:https://img.shields.io/twitter/follow/eddyverbruggen.svg?style=social&label=Follow%20me
11 | [twitter-url]:https://twitter.com/eddyverbruggen
12 |
13 | You can use this plugin to compose an e-mail, have the user edit the draft manually, and send it.
14 |
15 | > Note that this plugin depends on the default mail app. If you want a fallback to a third party client app like Gmail or Outlook, then check for availability, and if not available use a solution like [the Social Share plugin](https://github.com/tjvantoll/nativescript-social-share).
16 |
17 | > ⚠️ Looking for NativeScript 7 compatibilty? Go to [the NativeScript/plugins repo](https://github.com/NativeScript/plugins/tree/master/packages/email).
18 |
19 | ## Installation
20 | Run this command from the root of your project:
21 |
22 | ```bash
23 | tns plugin add nativescript-email
24 | ```
25 |
26 | ## API
27 |
28 | To use this plugin you must first require/import it:
29 |
30 | #### TypeScript
31 |
32 | ```typescript
33 | import * as email from "nativescript-email";
34 | // or
35 | import { compose } from "nativescript-email";
36 | // or even
37 | import { compose as composeEmail } from "nativescript-email";
38 | ```
39 |
40 | #### JavaScript
41 |
42 | ```js
43 | var email = require("nativescript-email");
44 | ```
45 |
46 | ### `available`
47 |
48 | #### TypeScript
49 |
50 | ```typescript
51 | email.available().then((avail: boolean) => {
52 | console.log("Email available? " + avail);
53 | })
54 | ```
55 |
56 | #### JavaScript
57 |
58 | ```js
59 | email.available().then(function(avail) {
60 | console.log("Email available? " + avail);
61 | })
62 | ```
63 |
64 | ### `compose`
65 |
66 | #### JavaScript
67 |
68 | ```js
69 | // let's first create a File object using the tns file module
70 | var fs = require("file-system");
71 | var appPath = fs.knownFolders.currentApp().path;
72 | var logoPath = appPath + "/res/telerik-logo.png";
73 |
74 | email.compose({
75 | subject: "Yo",
76 | body: "Hello dude :)",
77 | to: ['eddyverbruggen@gmail.com', 'to@person2.com'],
78 | cc: ['ccperson@somewhere.com'],
79 | bcc: ['eddy@combidesk.com', 'eddy@x-services.nl'],
80 | attachments: [
81 | {
82 | fileName: 'arrow1.png',
83 | path: 'base64://iVBORw0KGgoAAAANSUhEUgAAABYAAAAoCAYAAAD6xArmAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAABQAAAAoAAAAFAAAABQAAAB5EsHiAAAAAEVJREFUSA1iYKAimDhxYjwIU9FIBgaQgZMmTfoPwlOmTJGniuHIhlLNxaOGwiNqNEypkwlGk9RokoIUfaM5ijo5Clh9AAAAAP//ksWFvgAAAEFJREFUY5g4cWL8pEmT/oMwiM1ATTBqONbQHA2W0WDBGgJYBUdTy2iwYA0BrILDI7VMmTJFHqv3yBUEBQsIg/QDAJNpcv6v+k1ZAAAAAElFTkSuQmCC',
84 | mimeType: 'image/png'
85 | },
86 | {
87 | fileName: 'telerik-logo.png',
88 | path: logoPath,
89 | mimeType: 'image/png'
90 | }]
91 | }).then(
92 | function() {
93 | console.log("Email composer closed");
94 | }, function(err) {
95 | console.log("Error: " + err);
96 | });
97 | ```
98 |
99 | Full attachment support has been added to 1.3.0 per the example above.
100 |
101 | Since 1.4.0 the promise will be rejected in case a file can't be found.
102 |
103 | ## Usage with Angular
104 | Check out [this tutorial (YouTube)](https://www.youtube.com/watch?v=fSnQb9-Gtdk) to learn how to use this plugin in a NativeScript-Angular app.
105 |
106 | ## Known issues
107 | On iOS you can't use the simulator to test the plugin because of an iOS limitation.
108 | To prevent a crash this plugin returns `false` when `available` is invoked on the iOS sim.
109 |
--------------------------------------------------------------------------------
/email.android.js:
--------------------------------------------------------------------------------
1 | var application = require("tns-core-modules/application");
2 | var fs = require("tns-core-modules/file-system");
3 |
4 | (function () {
5 | _cleanAttachmentFolder();
6 | })();
7 |
8 | var _determineAvailability = function () {
9 | var uri = android.net.Uri.fromParts("mailto", "", null);
10 | var intent = new android.content.Intent(android.content.Intent.ACTION_SENDTO, uri);
11 | var packageManager = application.android.context.getPackageManager();
12 | var nrOfMailApps = packageManager.queryIntentActivities(intent, 0).size();
13 | return nrOfMailApps > 0;
14 | };
15 |
16 | exports.available = function () {
17 | return new Promise(function (resolve, reject) {
18 | try {
19 | resolve(_determineAvailability());
20 | } catch (ex) {
21 | console.log("Error in email.available: " + ex);
22 | reject(ex);
23 | }
24 | });
25 | };
26 |
27 | exports.compose = function (arg) {
28 | return new Promise(function (resolve, reject) {
29 | try {
30 |
31 | if (!_determineAvailability()) {
32 | reject("No mail available");
33 | }
34 |
35 | var mail = new android.content.Intent(android.content.Intent.ACTION_SENDTO);
36 | if (arg.body) {
37 | var htmlPattern = java.util.regex.Pattern.compile(".*\\<[^>]+>.*", java.util.regex.Pattern.DOTALL);
38 | if (htmlPattern.matcher(arg.body).matches()) {
39 | mail.putExtra(android.content.Intent.EXTRA_TEXT, android.text.Html.fromHtml(arg.body));
40 | mail.setType("text/html");
41 | } else {
42 | mail.putExtra(android.content.Intent.EXTRA_TEXT, arg.body);
43 | mail.setType("text/plain");
44 | }
45 | }
46 |
47 | if (arg.subject) {
48 | mail.putExtra(android.content.Intent.EXTRA_SUBJECT, arg.subject);
49 | }
50 | if (arg.to) {
51 | mail.putExtra(android.content.Intent.EXTRA_EMAIL, toStringArray(arg.to));
52 | }
53 | if (arg.cc) {
54 | mail.putExtra(android.content.Intent.EXTRA_CC, toStringArray(arg.cc));
55 | }
56 | if (arg.bcc) {
57 | mail.putExtra(android.content.Intent.EXTRA_BCC, toStringArray(arg.bcc));
58 | }
59 |
60 | if (arg.attachments) {
61 | var uris = new java.util.ArrayList();
62 | for (var a in arg.attachments) {
63 | var attachment = arg.attachments[a];
64 | var path = attachment.path;
65 | var fileName = attachment.fileName;
66 | var uri = _getUriForPath(path, fileName, application.android.context);
67 |
68 | if (!uri) {
69 | reject("File not found for path: " + path);
70 | return;
71 | }
72 | uris.add(uri);
73 | }
74 |
75 | if (!uris.isEmpty()) {
76 | // required for Android 7+ (alternative is using a FileProvider (which is a better solution btw))
77 | var builder = new android.os.StrictMode.VmPolicy.Builder();
78 | android.os.StrictMode.setVmPolicy(builder.build());
79 |
80 | mail.setAction(android.content.Intent.ACTION_SEND_MULTIPLE);
81 | mail.setType("message/rfc822");
82 | mail.putParcelableArrayListExtra(android.content.Intent.EXTRA_STREAM, uris);
83 | }
84 | } else {
85 | mail.setData(android.net.Uri.parse("mailto:"));
86 | }
87 |
88 | mail.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
89 |
90 | // we can wire up an intent receiver but it's always the same resultCode (0, canceled) anyway
91 | application.android.context.startActivity(mail);
92 | resolve(true);
93 | } catch (ex) {
94 | console.log("Error in email.compose: " + ex);
95 | reject(ex);
96 | }
97 | });
98 | };
99 |
100 | function _getUriForPath(path, fileName, ctx) {
101 | if (path.indexOf("file:///") === 0) {
102 | return _getUriForAbsolutePath(path);
103 | } else if (path.indexOf("file://") === 0) {
104 | return _getUriForAssetPath(path, fileName, ctx);
105 | } else if (path.indexOf("base64:") === 0) {
106 | return _getUriForBase64Content(path, fileName, ctx);
107 | } else {
108 | if (path.indexOf(ctx.getPackageName()) > -1) {
109 | return _getUriForAssetPath(path, fileName, ctx);
110 | } else {
111 | return _getUriForAbsolutePath(path);
112 | }
113 | }
114 | }
115 |
116 | function _getUriForAbsolutePath(path) {
117 | var absPath = path.replace("file://", "");
118 | var file = new java.io.File(absPath);
119 | if (!file.exists()) {
120 | console.log("File not found: " + file.getAbsolutePath());
121 | return null;
122 | } else {
123 | return android.net.Uri.fromFile(file);
124 | }
125 | }
126 |
127 | function _getUriForAssetPath(path, fileName, ctx) {
128 | path = path.replace("file://", "/");
129 | if (!fs.File.exists(path)) {
130 | console.log("File does not exist: " + path);
131 | return null;
132 | }
133 |
134 | var localFile = fs.File.fromPath(path);
135 | var localFileContents = localFile.readSync(function (e) {
136 | error = e;
137 | });
138 |
139 | var cacheFileName = _writeBytesToFile(ctx, fileName, localFileContents);
140 | if (cacheFileName.indexOf("file://") === -1) {
141 | cacheFileName = "file://" + cacheFileName;
142 | }
143 | return android.net.Uri.parse(cacheFileName);
144 | }
145 |
146 | function _getUriForBase64Content(path, fileName, ctx) {
147 | var resData = path.substring(path.indexOf("://") + 3);
148 | var bytes;
149 | try {
150 | bytes = android.util.Base64.decode(resData, 0);
151 | } catch (ex) {
152 | console.log("Invalid Base64 string: " + resData);
153 | return android.net.Uri.EMPTY;
154 | }
155 | var cacheFileName = _writeBytesToFile(ctx, fileName, bytes);
156 |
157 | return android.net.Uri.parse(cacheFileName);
158 | }
159 |
160 | function _writeBytesToFile(ctx, fileName, contents) {
161 | var dir = ctx.getExternalCacheDir();
162 |
163 | if (dir === null) {
164 | console.log("Missing external cache dir");
165 | return null;
166 | }
167 |
168 | var storage = dir.toString() + "/emailcomposer";
169 | var cacheFileName = storage + "/" + fileName;
170 |
171 | var toFile = fs.File.fromPath(cacheFileName);
172 | toFile.writeSync(contents, function (e) {
173 | error = e;
174 | });
175 |
176 | if (cacheFileName.indexOf("file://") === -1) {
177 | cacheFileName = "file://" + cacheFileName;
178 | }
179 | return cacheFileName;
180 | }
181 |
182 | function _cleanAttachmentFolder() {
183 |
184 | if (application.android.context) {
185 | var dir = application.android.context.getExternalCacheDir();
186 |
187 | if (dir === null) {
188 | console.log("Missing external cache dir");
189 | return;
190 | }
191 |
192 | var storage = dir.toString() + "/emailcomposer";
193 | var cacheFolder = fs.Folder.fromPath(storage);
194 | cacheFolder.clear();
195 | }
196 | }
197 |
198 | var toStringArray = function (arg) {
199 | var arr = java.lang.reflect.Array.newInstance(java.lang.String.class, arg.length);
200 | for (var i = 0; i < arg.length; i++) {
201 | arr[i] = arg[i];
202 | }
203 | return arr;
204 | };
205 |
--------------------------------------------------------------------------------
/email.ios.js:
--------------------------------------------------------------------------------
1 | var fs = require("tns-core-modules/file-system");
2 | var frame = require("tns-core-modules/ui/frame");
3 |
4 | var _determineAvailability = function () {
5 | var isSimulator = NSProcessInfo.processInfo.environment.objectForKey("SIMULATOR_DEVICE_NAME") !== null;
6 |
7 | if (isSimulator) {
8 | console.log("Email is not available on the Simulator");
9 | }
10 |
11 | return !isSimulator && MFMailComposeViewController.canSendMail();
12 | };
13 |
14 | exports.available = function () {
15 | return new Promise(function (resolve, reject) {
16 | try {
17 | resolve(_determineAvailability());
18 | } catch (ex) {
19 | console.log("Error in email.available: " + ex);
20 | reject(ex);
21 | }
22 | });
23 | };
24 |
25 | exports.compose = function (arg) {
26 | return new Promise(function (resolve, reject) {
27 | try {
28 |
29 | if (!_determineAvailability()) {
30 | reject("No mail available");
31 | return;
32 | }
33 |
34 | var topMostFrame = frame.topmost();
35 | if (topMostFrame) {
36 | var viewController = topMostFrame.currentPage && topMostFrame.currentPage.ios;
37 | if (viewController) {
38 | while (viewController.parentViewController) {
39 | viewController = viewController.parentViewController;
40 | }
41 | while (viewController.presentedViewController) {
42 | viewController = viewController.presentedViewController;
43 | }
44 | }
45 | }
46 |
47 | var mail = MFMailComposeViewController.new();
48 |
49 | var message = arg.body;
50 | if (message) {
51 | var messageAsNSString = NSString.stringWithString(message);
52 | var isHTML = messageAsNSString.rangeOfStringOptions("<[^>]+>", NSRegularExpressionSearch).location !== NSNotFound;
53 | mail.setMessageBodyIsHTML(arg.body, isHTML);
54 | }
55 | mail.setSubject(arg.subject);
56 | mail.setToRecipients(arg.to);
57 | mail.setCcRecipients(arg.cc);
58 | mail.setBccRecipients(arg.bcc);
59 |
60 | if (arg.attachments) {
61 | for (var a in arg.attachments) {
62 | var attachment = arg.attachments[a];
63 | var path = attachment.path;
64 | var data = _getDataForAttachmentPath(path);
65 | if (data === null) {
66 | reject("File not found for path: " + path);
67 | return;
68 | } else if (!attachment.fileName) {
69 | console.log("attachment.fileName is mandatory");
70 | } else if (!attachment.mimeType) {
71 | console.log("attachment.mimeType is mandatory");
72 | } else {
73 | mail.addAttachmentDataMimeTypeFileName(
74 | data, attachment.mimeType, attachment.fileName);
75 | }
76 | }
77 | }
78 |
79 | // Assign first to local variable, otherwise it will be garbage collected since delegate is weak reference.
80 | var delegate = MFMailComposeViewControllerDelegateImpl.new().initWithCallback(function (result, error) {
81 | // invoke the callback / promise
82 | resolve(result === MFMailComposeResult.Sent);
83 | // close the mail
84 | viewController.dismissViewControllerAnimatedCompletion(true, null);
85 | // release the delegate instance
86 | CFRelease(delegate);
87 | });
88 |
89 | // retain the delegate because the mailComposeDelegate property won't do it for us
90 | CFRetain(delegate);
91 |
92 | mail.mailComposeDelegate = delegate;
93 |
94 | viewController.presentViewControllerAnimatedCompletion(mail, true, null);
95 |
96 | } catch (ex) {
97 | console.log("Error in email.compose: " + ex);
98 | reject(ex);
99 | }
100 | });
101 | };
102 |
103 | function _getDataForAttachmentPath(path) {
104 | var data = null;
105 | if (path.indexOf("file:///") === 0) {
106 | data = _dataForAbsolutePath(path);
107 | } else if (path.indexOf("file://") === 0) {
108 | data = _dataForAsset(path);
109 | } else if (path.indexOf("base64:") === 0) {
110 | data = _dataFromBase64(path);
111 | } else {
112 | var fileManager = NSFileManager.defaultManager;
113 | if (fileManager.fileExistsAtPath(path)) {
114 | data = fileManager.contentsAtPath(path);
115 | }
116 | }
117 | return data;
118 | }
119 |
120 | function _dataFromBase64(base64String) {
121 | base64String = base64String.substring(base64String.indexOf("://") + 3);
122 | return NSData.alloc().initWithBase64EncodedStringOptions(base64String, 0);
123 | }
124 |
125 | function _dataForAsset(path) {
126 | path = path.replace("file://", "/");
127 |
128 | if (!fs.File.exists(path)) {
129 | console.log("File does not exist: " + path);
130 | return null;
131 | }
132 |
133 | var localFile = fs.File.fromPath(path);
134 | return localFile.readSync(function (e) {
135 | error = e;
136 | });
137 | }
138 |
139 | function _dataForAbsolutePath(path) {
140 | var fileManager = NSFileManager.defaultManager;
141 | var absPath = path.replace("file://", "");
142 |
143 | if (!fileManager.fileExistsAtPath(absPath)) {
144 | console.log("File not found: " + absPath);
145 | return null;
146 | }
147 |
148 | return fileManager.contentsAtPath(absPath);
149 | }
150 |
151 | var MFMailComposeViewControllerDelegateImpl = (function (_super) {
152 | __extends(MFMailComposeViewControllerDelegateImpl, _super);
153 |
154 | function MFMailComposeViewControllerDelegateImpl() {
155 | _super.apply(this, arguments);
156 | }
157 |
158 | MFMailComposeViewControllerDelegateImpl.new = function () {
159 | return _super.new.call(this);
160 | };
161 | MFMailComposeViewControllerDelegateImpl.prototype.initWithCallback = function (callback) {
162 | this._callback = callback;
163 | return this;
164 | };
165 | MFMailComposeViewControllerDelegateImpl.prototype.mailComposeControllerDidFinishWithResultError = function (controller, result, error) {
166 | this._callback(result, error);
167 | };
168 | MFMailComposeViewControllerDelegateImpl.ObjCProtocols = [MFMailComposeViewControllerDelegate];
169 | return MFMailComposeViewControllerDelegateImpl;
170 | })(NSObject);
171 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export interface Attachment {
2 | /**
3 | * The name used for the attachment.
4 | * Example:
5 | *
6 | * fileName: 'Cute-Kitten.png'
7 | */
8 | fileName: string;
9 |
10 | /**
11 | * There are various ways to use the path param:
12 | *
13 | * - base64 encoded: 'base64://iVBORw..XYZ'
14 | * - local app folder 'file://..
15 | * - anywhere on the device: 'file:///..'
16 | * - or '/some/path/to/file.png'
17 | */
18 | path: string;
19 |
20 | /**
21 | * Used to help the iOS figure out how to send the file (not used on Android).
22 | * Example:
23 | *
24 | * mimeType: 'image/png'
25 | */
26 | mimeType: string;
27 | }
28 |
29 | /**
30 | * The options object passed into the compose function.
31 | */
32 | export interface ComposeOptions {
33 | /**
34 | * The subject of your email.
35 | */
36 | subject?: string;
37 |
38 | /**
39 | * The plugin will automatically handle plain and html email content.
40 | */
41 | body?: string;
42 |
43 | /**
44 | * A string array of email addresses.
45 | * Known issue: on Android only the first item in the array is added.
46 | */
47 | to?: string[];
48 |
49 | /**
50 | * A string array of email addresses.
51 | * Known issue: on Android only the first item in the array is added.
52 | */
53 | cc?: string[];
54 |
55 | /**
56 | * A string array of email addresses.
57 | * Known issue: on Android only the first item in the array is added.
58 | */
59 | bcc?: string[];
60 |
61 | /**
62 | * An optional Array of attachments.
63 | */
64 | attachments?: Array;
65 |
66 | /**
67 | * @deprecated No longer used, but keeping it around to notify you.
68 | */
69 | appPickerTitle?: string;
70 | }
71 |
72 | /**
73 | * No email client may be available, so test first.
74 | */
75 | export function available(): Promise;
76 |
77 | /**
78 | * On iOS the returned boolean indicates whether or not the email was sent by the user.
79 | * On Android it's always true, unfortunately.
80 | */
81 | export function compose(options: ComposeOptions): Promise;
82 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nativescript-email",
3 | "version": "1.6.0",
4 | "description": "Email plugin for your NativeScript app",
5 | "main": "email",
6 | "typings": "index.d.ts",
7 | "nativescript": {
8 | "platforms": {
9 | "ios": "2.3.0",
10 | "android": "2.3.0"
11 | }
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/EddyVerbruggen/nativescript-email.git"
16 | },
17 | "keywords": [
18 | "ecosystem:NativeScript",
19 | "NativeScript",
20 | "Mail",
21 | "Email",
22 | "E-mail",
23 | "Draft",
24 | "Compose",
25 | "MailComposer",
26 | "Attachment"
27 | ],
28 | "author": "Eddy Verbruggen (https://github.com/EddyVerbruggen)",
29 | "contributors": [
30 | {
31 | "name": "Brad Martin",
32 | "email": "bradwaynemartin@gmail.com",
33 | "url": "https://github.com/BradMartin"
34 | }
35 | ],
36 | "devDependencies": {
37 | "tns-core-modules": "^6.0.4",
38 | "tns-platform-declarations": "~6.0.4"
39 | },
40 | "license": "MIT",
41 | "bugs": "https://github.com/eddyverbruggen/nativescript-email/issues",
42 | "homepage": "https://github.com/eddyverbruggen/nativescript-email"
43 | }
44 |
--------------------------------------------------------------------------------
/references.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
--------------------------------------------------------------------------------