diff --git a/README.md b/README.md index 3bf1f4c..023ed23 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,11 @@ public class MainApplication extends Application implements ReactApplication { ``` +#### File Attachments on Android + +Addition setup is required for Android file sharing, see https://developer.android.com/training/secure-file-sharing/setup-sharing + +It is expected that the `android:authorities` property is set to the app's package name with ".fileprovider" appended to the end. ### Manual Installation: iOS @@ -162,7 +167,7 @@ export default class App extends Component { ### Note -On Android, the `callback` will only be called if an `error` occurs. The `event` argument is unused! +On Android, the `callback` will only be called if an `error` occurs. The `event` argument will contain a string with the exception or error message. ## Here is how it looks: ![Demo gif](https://github.com/chirag04/react-native-mail/blob/master/screenshot.png) diff --git a/RNMail/RNMail.m b/RNMail/RNMail.m index 605c373..ff7cb10 100644 --- a/RNMail/RNMail.m +++ b/RNMail/RNMail.m @@ -41,7 +41,7 @@ + (BOOL)requiresMainQueueSetup NSString *subject = [RCTConvert NSString:options[@"subject"]]; [mail setSubject:subject]; } - + bool *isHTML = NO; if (options[@"isHTML"]){ @@ -57,7 +57,7 @@ + (BOOL)requiresMainQueueSetup NSArray *recipients = [RCTConvert NSArray:options[@"recipients"]]; [mail setToRecipients:recipients]; } - + if (options[@"ccRecipients"]){ NSArray *ccRecipients = [RCTConvert NSArray:options[@"ccRecipients"]]; [mail setCcRecipients:ccRecipients]; @@ -67,67 +67,70 @@ + (BOOL)requiresMainQueueSetup NSArray *bccRecipients = [RCTConvert NSArray:options[@"bccRecipients"]]; [mail setBccRecipients:bccRecipients]; } - - if (options[@"attachment"] && options[@"attachment"][@"path"] && options[@"attachment"][@"type"]){ - NSString *attachmentPath = [RCTConvert NSString:options[@"attachment"][@"path"]]; - NSString *attachmentType = [RCTConvert NSString:options[@"attachment"][@"type"]]; - NSString *attachmentName = [RCTConvert NSString:options[@"attachment"][@"name"]]; - - // Set default filename if not specificed - if (!attachmentName) { - attachmentName = [[attachmentPath lastPathComponent] stringByDeletingPathExtension]; - } - - // Get the resource path and read the file using NSData - NSData *fileData = [NSData dataWithContentsOfFile:attachmentPath]; - - // Determine the MIME type - NSString *mimeType; - - /* - * Add additional mime types and PR if necessary. Find the list - * of supported formats at http://www.iana.org/assignments/media-types/media-types.xhtml - */ - if ([attachmentType isEqualToString:@"jpg"]) { - mimeType = @"image/jpeg"; - } else if ([attachmentType isEqualToString:@"png"]) { - mimeType = @"image/png"; - } else if ([attachmentType isEqualToString:@"doc"]) { - mimeType = @"application/msword"; - } else if ([attachmentType isEqualToString:@"ppt"]) { - mimeType = @"application/vnd.ms-powerpoint"; - } else if ([attachmentType isEqualToString:@"html"]) { - mimeType = @"text/html"; - } else if ([attachmentType isEqualToString:@"csv"]) { - mimeType = @"text/csv"; - } else if ([attachmentType isEqualToString:@"pdf"]) { - mimeType = @"application/pdf"; - } else if ([attachmentType isEqualToString:@"vcard"]) { - mimeType = @"text/vcard"; - } else if ([attachmentType isEqualToString:@"json"]) { - mimeType = @"application/json"; - } else if ([attachmentType isEqualToString:@"zip"]) { - mimeType = @"application/zip"; - } else if ([attachmentType isEqualToString:@"text"]) { - mimeType = @"text/*"; - } else if ([attachmentType isEqualToString:@"mp3"]) { - mimeType = @"audio/mpeg"; - } else if ([attachmentType isEqualToString:@"wav"]) { - mimeType = @"audio/wav"; - } else if ([attachmentType isEqualToString:@"aiff"]) { - mimeType = @"audio/aiff"; - } else if ([attachmentType isEqualToString:@"flac"]) { - mimeType = @"audio/flac"; - } else if ([attachmentType isEqualToString:@"ogg"]) { - mimeType = @"audio/ogg"; - } else if ([attachmentType isEqualToString:@"xls"]) { - mimeType = @"application/vnd.ms-excel"; + + if (options[@"attachments"]){ + NSArray *attachments = [RCTConvert NSArray:options[@"attachments"]]; + + for(NSDictionary *attachment in attachments) { + if (attachment[@"path"] && attachment[@"type"]) { + NSString *attachmentPath = [RCTConvert NSString:attachment[@"path"]]; + NSString *attachmentType = [RCTConvert NSString:attachment[@"type"]]; + NSString *attachmentName = [RCTConvert NSString:attachment[@"name"]]; + + // Set default filename if not specificed + if (!attachmentName) { + attachmentName = [[attachmentPath lastPathComponent] stringByDeletingPathExtension]; + } + // Get the resource path and read the file using NSData + NSData *fileData = [NSData dataWithContentsOfFile:attachmentPath]; + + // Determine the MIME type + NSString *mimeType; + + /* + * Add additional mime types and PR if necessary. Find the list + * of supported formats at http://www.iana.org/assignments/media-types/media-types.xhtml + */ + if ([attachmentType isEqualToString:@"jpg"]) { + mimeType = @"image/jpeg"; + } else if ([attachmentType isEqualToString:@"png"]) { + mimeType = @"image/png"; + } else if ([attachmentType isEqualToString:@"doc"]) { + mimeType = @"application/msword"; + } else if ([attachmentType isEqualToString:@"ppt"]) { + mimeType = @"application/vnd.ms-powerpoint"; + } else if ([attachmentType isEqualToString:@"html"]) { + mimeType = @"text/html"; + } else if ([attachmentType isEqualToString:@"csv"]) { + mimeType = @"text/csv"; + } else if ([attachmentType isEqualToString:@"pdf"]) { + mimeType = @"application/pdf"; + } else if ([attachmentType isEqualToString:@"vcard"]) { + mimeType = @"text/vcard"; + } else if ([attachmentType isEqualToString:@"json"]) { + mimeType = @"application/json"; + } else if ([attachmentType isEqualToString:@"zip"]) { + mimeType = @"application/zip"; + } else if ([attachmentType isEqualToString:@"text"]) { + mimeType = @"text/*"; + } else if ([attachmentType isEqualToString:@"mp3"]) { + mimeType = @"audio/mpeg"; + } else if ([attachmentType isEqualToString:@"wav"]) { + mimeType = @"audio/wav"; + } else if ([attachmentType isEqualToString:@"aiff"]) { + mimeType = @"audio/aiff"; + } else if ([attachmentType isEqualToString:@"flac"]) { + mimeType = @"audio/flac"; + } else if ([attachmentType isEqualToString:@"ogg"]) { + mimeType = @"audio/ogg"; + } else if ([attachmentType isEqualToString:@"xls"]) { + mimeType = @"application/vnd.ms-excel"; + } + [mail addAttachmentData:fileData mimeType:mimeType fileName:attachmentName]; + } } - - // Add attachment - [mail addAttachmentData:fileData mimeType:mimeType fileName:attachmentName]; } - + UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; while (root.presentedViewController) { diff --git a/android/src/main/java/com/chirag/RNMail/RNMailModule.java b/android/src/main/java/com/chirag/RNMail/RNMailModule.java index 658ce6a..d5ff434 100644 --- a/android/src/main/java/com/chirag/RNMail/RNMailModule.java +++ b/android/src/main/java/com/chirag/RNMail/RNMailModule.java @@ -4,17 +4,27 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.support.v4.content.FileProvider; import android.text.Html; +import android.util.Log; +import com.facebook.common.file.FileUtils; +import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReadableMap; -import java.util.List; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + /** * NativeModule that allows JS to open emails sending apps chooser. @@ -34,12 +44,11 @@ public String getName() { } /** - * Converts a ReadableArray to a String array - * - * @param r the ReadableArray instance to convert - * - * @return array of strings - */ + * Converts a ReadableArray to a String array + * + * @param r the ReadableArray instance to convert + * @return array of strings + */ private String[] readableArrayToStringArray(ReadableArray r) { int length = r.size(); String[] strArray = new String[length]; @@ -53,8 +62,8 @@ private String[] readableArrayToStringArray(ReadableArray r) { @ReactMethod public void mail(ReadableMap options, Callback callback) { - Intent i = new Intent(Intent.ACTION_SENDTO); - i.setData(Uri.parse("mailto:")); + Intent i = new Intent(Intent.ACTION_SEND_MULTIPLE); + i.setType("message/rfc822"); if (options.hasKey("subject") && !options.isNull("subject")) { i.putExtra(Intent.EXTRA_SUBJECT, options.getString("subject")); @@ -66,13 +75,13 @@ public void mail(ReadableMap options, Callback callback) { i.putExtra(Intent.EXTRA_TEXT, Html.fromHtml(body)); } else { i.putExtra(Intent.EXTRA_TEXT, body); - } + } } if (options.hasKey("recipients") && !options.isNull("recipients")) { ReadableArray recipients = options.getArray("recipients"); i.putExtra(Intent.EXTRA_EMAIL, readableArrayToStringArray(recipients)); - } + } if (options.hasKey("ccRecipients") && !options.isNull("ccRecipients")) { ReadableArray ccRecipients = options.getArray("ccRecipients"); @@ -84,14 +93,57 @@ public void mail(ReadableMap options, Callback callback) { i.putExtra(Intent.EXTRA_BCC, readableArrayToStringArray(bccRecipients)); } - if (options.hasKey("attachment") && !options.isNull("attachment")) { - ReadableMap attachment = options.getMap("attachment"); - if (attachment.hasKey("path") && !attachment.isNull("path")) { - String path = attachment.getString("path"); - File file = new File(path); - Uri p = Uri.fromFile(file); - i.putExtra(Intent.EXTRA_STREAM, p); + if (options.hasKey("attachments") && !options.isNull("attachments")) { + + ReadableArray r = options.getArray("attachments"); + int length = r.size(); + ArrayList uris = new ArrayList(); + for (int keyIndex = 0; keyIndex < length; keyIndex++) { + ReadableMap clip = r.getMap(keyIndex); + if (clip.hasKey("path") && !clip.isNull("path")) { + String path = clip.getString("path"); + Log.d ("RNMail", "Attachment file path: " + path); + + File file = new File(path); + + String name, suffix = ""; + if (clip.hasKey("name")) + name = clip.getString("name"); + else + name = file.getName(); + + if (clip.hasKey("type")) + suffix = "." + clip.getString("type"); + + + file.setReadable(true, false); + if (file.exists()) { + + if (file.length() == 0) + Log.d ("RNMail", "Warning, attaching empty file!"); + // Use the FileProvider to get a content URI + try { + Uri fileUri = FileProvider.getUriForFile( + getCurrentActivity(), + reactContext.getPackageName() + ".fileprovider", + file); + if (fileUri != null) { + // Grant temporary read permission to the content URI + uris.add(fileUri); + } + } catch (Exception e) { + String message = "There was a problem sharing the file " + file.getName(); + Log.e("RNMail", message); + callback.invoke("error", message + "\n" + e.getMessage()); + } + } else { + Log.e("RNMail", "Attachment file does not exist"); + } + } } + i.setType("*/*"); + i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } PackageManager manager = reactContext.getPackageManager(); @@ -106,18 +158,18 @@ public void mail(ReadableMap options, Callback callback) { i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { reactContext.startActivity(i); - } catch (Exception ex) { - callback.invoke("error"); + } catch (Exception e) { + callback.invoke("error", e.getMessage()); } } else { - Intent chooser = Intent.createChooser(i, "Send Mail"); + Intent chooser = Intent.createChooser(i, "Send email..."); chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { reactContext.startActivity(chooser); - } catch (Exception ex) { - callback.invoke("error"); + } catch (Exception e) { + callback.invoke("error", e.getMessage()); } + } } }