├── .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 | /// --------------------------------------------------------------------------------