Android Studio中SMTP发送多图附件遇FileNotFoundException求助
我帮你排查到问题所在了——你遇到的FileNotFoundException: /external/images/media/39235: open failed: ENOENT (No such file or directory),核心原因是:
通过ACTION_PICK从相册选择图片后拿到的是Content URI(格式类似content://external/images/media/39235),而不是真实的文件系统路径。你直接调用Uri.getPath()得到的字符串并不是可直接访问的文件路径,再加上Android 10(API 29)及以上的Scoped Storage机制限制APP直接访问外部存储文件,自然会提示文件不存在。
核心解决思路
不要尝试通过Content URI获取真实文件路径,而是直接通过ContentResolver打开该URI对应的输入流,用输入流来构建邮件附件的数据源,这样既能适配Scoped Storage,也能兼容所有Android版本。
修改你的代码
1. 修改发送邮件的附件处理逻辑
把原来用FileDataSource的部分替换成基于输入流的数据源实现,这里我们可以用JavaMail的ByteArrayDataSource,或者自定义一个基于输入流的DataSource(如果ByteArrayDataSource不可用的话)。
首先,确保你能拿到有效的Context(比如Activity的实例或者Application Context),然后修改附件循环的代码:
// 替换原来的附件循环部分 if (!arrayUri.isEmpty()) { Multipart multipart = new MimeMultipart(); ContentResolver resolver = getContentResolver(); // 这里要确保是Activity的ContentResolver,或者传入Context获取 for (Uri uri : arrayUri) { MimeBodyPart messageBodyPart2 = new MimeBodyPart(); // 获取文件名:通过ContentResolver查询MediaStore的列来得到真实文件名 String[] projection = {MediaStore.Images.Media.DISPLAY_NAME}; Cursor cursor = resolver.query(uri, projection, null, null, null); String filename = "unknown_image.jpg"; if (cursor != null && cursor.moveToFirst()) { int nameIndex = cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME); filename = cursor.getString(nameIndex); cursor.close(); } // 从Content URI获取输入流 try (InputStream inputStream = resolver.openInputStream(uri)) { // 用ByteArrayDataSource包装输入流 DataSource source = new ByteArrayDataSource(inputStream, resolver.getType(uri)); messageBodyPart2.setDataHandler(new DataHandler(source)); messageBodyPart2.setFileName(filename); messageBodyPart2.setDisposition(MimeBodyPart.ATTACHMENT); multipart.addBodyPart(messageBodyPart2); } catch (IOException e) { e.printStackTrace(); } } message.setContent(multipart); }
2. 权限适配(重要)
- Android 6.0(API 23)到Android 12(API 32):需要动态申请
READ_EXTERNAL_STORAGE权限 - Android 13(API 33)及以上:需要动态申请
READ_MEDIA_IMAGES权限
你需要在代码中添加权限申请逻辑,确保用户授权后再进行选图和发送操作。
3. 选图代码无需修改
你当前的ACTION_PICK选图逻辑是正确的,不需要改动,只要确保拿到的Uri是有效的即可。
额外说明
- 为什么不直接获取真实文件路径?
在Android 10及以上的Scoped Storage机制下,APP无法直接访问外部存储的文件路径(除了APP私有目录),即使通过ContentResolver查询到路径,也可能没有访问权限,所以用输入流是最稳妥的方案。 - 如果你的项目无法引入
ByteArrayDataSource,可以自定义一个基于输入流的DataSource:
public class InputStreamDataSource implements DataSource { private InputStream inputStream; private String contentType; public InputStreamDataSource(InputStream inputStream, String contentType) { this.inputStream = inputStream; this.contentType = contentType; } @Override public InputStream getInputStream() throws IOException { return inputStream; } @Override public OutputStream getOutputStream() throws IOException { throw new UnsupportedOperationException("Not supported"); } @Override public String getContentType() { return contentType; } @Override public String getName() { return "InputStreamDataSource"; } }
使用时把ByteArrayDataSource替换成这个自定义类即可。
内容的提问来源于stack exchange,提问作者epssy_sy

