43

My app creates mails with attachments, and uses an intent with Intent.ACTION_SEND to launch a mail app.

It works with all the mail apps I tested with, except for the new Gmail 5.0 (it works with Gmail 4.9), where the mail opens without attachment, showing the error: "Permission denied for the attachment".

There are no useful messages from Gmail on logcat. I only tested Gmail 5.0 on Android KitKat, but on multiple devices.

I create the file for the attachment like this:

String fileName = "file-name_something_like_this";
FileOutputStream output = context.openFileOutput(
        fileName, Context.MODE_WORLD_READABLE);

// Write data to output...

output.close();
File fileToSend = new File(context.getFilesDir(), fileName);

I'm aware of the security concerns with MODE_WORLD_READABLE.

I send the intent like this:

public static void compose(
        Context context,
        String address,
        String subject,
        String body,
        File attachment) {

    Intent emailIntent = new Intent(Intent.ACTION_SEND);
    emailIntent.setType("message/rfc822");
    emailIntent.putExtra(
            Intent.EXTRA_EMAIL, new String[] { address });
    emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
    emailIntent.putExtra(Intent.EXTRA_TEXT, body);

    emailIntent.putExtra(
            Intent.EXTRA_STREAM,
            Uri.fromFile(attachment));

    Intent chooser = Intent.createChooser(
            emailIntent, 
            context.getString(R.string.send_mail_chooser));

    context.startActivity(chooser);
}

Is there anything I do wrong when creating the file or sending the intent? Is there a better way to start a mail app with attachment? Alternatively - has someone encountered this problem and found a workaround for it?

Thanks!

natasky
  • 533
  • 1
  • 4
  • 5
  • @nobalG that has nothing to do with this issue. – Rolf ツ Nov 12 '14 at 09:27
  • 1
    I also experienced this issue and it seems the location of the attachment has something to do with the issue because sharing a attachment from the sd-card still works. (Al tough this should not matter because of the WORLD_READABLE flag) Could this be a bug? – Rolf ツ Nov 12 '14 at 09:28
  • Same issue here... I can still use ACTION_SEND to send files to Google Drive, but when I select GMail (with the new version) it fails like you saw. No problem with the same code on previous versions of GMail. – Wookie Nov 13 '14 at 20:33
  • Ran into same issue while working on share a local PDF file. Found this solution to get a basic FileProvider setup going using a minimal github example project: https://stackoverflow.com/a/48103567/2162226 , where adding and sending an attachment is part of the code there . It provides steps for which files to copy over (without making changes) into a local standalone project – Gene Bo Jan 04 '18 at 21:38

9 Answers9

31

I was able to pass a screenshot .jpeg file from my app to GMail 5.0 through an Intent. The key was in this answer.

Everything I have from @natasky 's code is nearly identical but instead, I have the file's directory as

context.getExternalCacheDir();

Which "represents the external storage directory where you should save cache files" (documentation)

Community
  • 1
  • 1
c0deblooded
  • 1,737
  • 1
  • 16
  • 20
  • This fixed my problem. After changing to startActivityForResult, I was getting an error in Gmail to the effect of "unable to attach an empty file" even though I could verify the file was not empty. The issue was that I was creating the file in the "getCacheDir()" directory. Changing to "getExternalCacheDir()" resulted in a successful attachment! – Andrew Aarestad Nov 19 '15 at 18:14
  • Thanks a lot, apparently this was the key to make it work in my app. – janos May 05 '16 at 09:41
  • 3
    This did it for me. I was actually using `File.createTempFile`, which accepts a third parameter for the location. I passed `activity.getExternalCacheDir()` for that third parameter and everything worked beautifully. Thanks! – Joshua Pinter May 17 '16 at 16:06
28

GMail 5.0 added some security checks to attachments it receives from an Intent. These are unrelated to unix permissions, so the fact that the file is readable doesn't matter.

When the attachment Uri is a file://, it'll only accept files from external storage, the private directory of gmail itself, or world-readable files from the private data directory of the calling app.

The problem with this security check is that it relies on gmail being able to find the caller app, which is only reliable when the caller has asked for result. In your code above, you do not ask for result and therefore gmail does not know who the caller is, and rejects your file.

Since it worked for you in 4.9 but not in 5.0, you know it's not a unix permission problem, so the reason must be the new checks.

TL;DR answer: replace startActivity with startActivityForResult.

Or better yet, use a content provider.

Andro Id
  • 1,008
  • 9
  • 6
  • Thank you, using startActivityForResult allows GMail to create the mail but it is not able to send it: libcore.io.ErrnoException: open failed: EACCES (Permission denied) at libcore.io.Posix.open(Native Method). I am creating attachment files via final File tempImage = File.createTempFile("image", ".jpg"); – Martin Vysny Jan 07 '15 at 12:07
  • 7
    i still get a permission denied toast when i try this. maybe content provider is the only way to go – David T. Jan 08 '15 at 01:49
  • In this case it really is a file permission issue. createTempFile creates a file that is only readable by the uid that created it. Not the issue described in this page. The problem described in this page is that the file was world-readable but gmail refused to read it due to its internal checks, and that's what startActivityForResult solves. But if your file isn't readable by gmail, you need to take care of that separately. – Andro Id Jan 08 '15 at 15:30
  • I had a world-readable file that was no longer readable, but replacing startActivity with startActivityForResult solved it for me. Thanks! – Hugo Logmans Apr 16 '15 at 08:50
  • Please provide some code snippet for it so we can use it – Anand Savjani Aug 13 '16 at 11:11
  • Your answer is not entirely accurate. It should be right to state that you need to use startActivityForResult to identify the application while having a file located in a publicly accessible directory (getCacheDir, getExternalCacheDir, etc.) – pudnivec74 Jul 11 '18 at 13:56
18

Use getExternalCacheDir() with File.createTempFile.

Use the following to create a temporary file in the external cache directory:

File tempFile = File.createTempFile("fileName", ".txt", context.getExternalCacheDir());

Then copy your original file's content to tempFile,

FileWriter fw = new FileWriter(tempFile);

FileReader fr = new FileReader(Data.ERR_BAK_FILE);
int c = fr.read();
while (c != -1) {
    fw.write(c);
    c = fr.read();
}
fr.close();

fw.flush();
fw.close();

now put your file to intent,

emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(tempFile));
Roshana Pitigala
  • 8,437
  • 8
  • 49
  • 80
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
  • Josh care to elaborate a bit more on your usage of this? I copied my file to a temporary file – wal Jun 30 '16 at 03:33
  • @wal I'm using this in two ways, one is to copy a file to the newly created tempfile with `FileUtils.copyFile(file, temporaryFile);`. Another way is creating the temp file and using a Buffer to fill that file with data. Does that help? – Joshua Pinter Jul 01 '16 at 20:29
  • This is just a workaround, unnecessarily copying data, taking double space, having to remember clearing the cache dir. Better use a FileProvider. – Miloš Černilovský Dec 18 '20 at 14:45
5

You should implement a FileProvider, which can create Uris for your app's internal files. Other apps are granted permission to read these Uris. Then, simply instead of calling Uri.fromFile(attachment), you instantiate your FileProvider and use:

fileProvider.getUriForFile(attachment);
Miloš Černilovský
  • 3,846
  • 1
  • 28
  • 30
4

Google have an answer for that issue:

  • Store the data in your own ContentProvider, making sure that other apps have the correct permission to access your provider. The preferred mechanism for providing access is to use per-URI permissions which are temporary and only grant access to the receiving application. An easy way to create a ContentProvider like this is to use the FileProvider helper class.

  • Use the system MediaStore. The MediaStore is primarily aimed at video, audio and image MIME types, however beginning with Android 3.0 (API level 11) it can also store non-media types (see MediaStore.Files for more info). Files can be inserted into the MediaStore using scanFile() after which a content:// style Uri suitable for sharing is passed to the provided onScanCompleted() callback. Note that once added to the system MediaStore the content is accessible to any app on the device.

Also you can try set permissions for your file:

emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

And finally you can copy/store your files in external storage - permissions not needed there.

Alexander Danilov
  • 3,038
  • 1
  • 30
  • 35
3

I tested it and I found out that it was definitely private storage access problem. When you attach some file to Gmail (over 5.0) do not use the file from private storage such as /data/data/package/. Try to use /storage/sdcard.

You can successfully attach your file.

Jared Burrows
  • 54,294
  • 25
  • 151
  • 185
2

Not sure why GMail 5.0 doesn't like certain file paths (which I've confirmed it does have read access to), but an apparently better solution is to implement your own ContentProvider class to serve the file. It's actually somewhat simple, and I found a decent example here: http://stephendnicholas.com/archives/974

Be sure to add the tag to your app manifest, and include a "android:grantUriPermissions="true"" within that. You'll also want to implement getType() and return the appropriate MIME type for the file URI, otherwise some apps wont work with this... There's an example of that in the comment section on the link.

Wookie
  • 782
  • 8
  • 12
1

I was having this problem and finally found an easy way to send email with attachment. Here is the code

public void SendEmail(){
    try {

        //saving image
        String randomNameOfPic = Calendar.DAY_OF_YEAR+DateFormat.getTimeInstance().toString();
        File file = new File(ActivityRecharge.this.getCacheDir(), "slip"+  randomNameOfPic+ ".jpg");
        FileOutputStream fOut = new FileOutputStream(file);
        myPic.compress(Bitmap.CompressFormat.JPEG, 100, fOut);
        fOut.flush();
        fOut.close();
        file.setReadable(true, false);

        //sending email
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("text/plain");
        intent.putExtra(Intent.EXTRA_EMAIL, new String[]{"zohabali5@gmail.com"});
        intent.putExtra(Intent.EXTRA_SUBJECT, "Recharge Account");
        intent.putExtra(Intent.EXTRA_TEXT, "body text");

        //Uri uri = Uri.parse("file://" + fileAbsolutePath);
        intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        startActivityForResult(Intent.createChooser(intent, "Send email..."),12);
    }catch (Exception e){
        Toast.makeText(ActivityRecharge.this,"Unable to open Email intent",Toast.LENGTH_LONG).show();
    }
}

In this code "myPic" is bitmap which was returned by camera intent

Zohab Ali
  • 8,426
  • 4
  • 55
  • 63
  • Thanks for your code. It works fine for me but with the note from Josh Pinter (see next answer). – Martin Jul 13 '17 at 14:32
0

Step 1: Add authority in your attached URI

Uri uri = FileProvider.getUriForFile(context, ""com.yourpackage", file);

Same as your manifest file provide name

android:authorities="com.yourpackage"

Step 2`; Add flag for allow to read

myIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

Ranjithkumar
  • 16,071
  • 12
  • 120
  • 159