【问题标题】:Send email with javax.mail using an existing InputStream as attachment content使用现有 InputStream 作为附件内容发送带有 javax.mail 的电子邮件
【发布时间】:2016-03-04 17:41:42
【问题描述】:

是否可以使用javax.mail 发送电子邮件并使用“现有”InputStream 作为电子邮件附件内容?

目前我正在按如下方式构建电子邮件:

final MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject("Subject line");

final Multipart multipartContent = new MimeMultipart();

    final MimeBodyPart textPart = new MimeBodyPart();
    textPart.setText("Message body");
    multipartContent.addBodyPart(textPart);

    final MimeBodyPart attachmentPart = new MimeBodyPart();
    final DataSource source = new InputStreamDataSource("text/plain", "test.txt", new ByteArrayInputStream("CONTENT INPUT STREAM".getBytes()));
    attachmentPart.setDataHandler(new DataHandler(source));
    attachmentPart.setFileName("text.txt");
    multipartContent.addBodyPart(attachmentPart);

message.setContent(multipartContent);

InputStreamDataSource实现如下:

public class InputStreamDataSource implements DataSource
{
    private final String contentType;
    private final String name;
    private final InputStream inputStream;

    public InputStreamDataSource(String contentType, String name, InputStream inputStream)
    {
        this.contentType = contentType;
        this.name = name;
        this.inputStream = inputStream;
    }

    public String getContentType()
    {
        return contentType;
    }

    public String getName()
    {
        return name;
    }

    public InputStream getInputStream() throws IOException
    {
        System.out.println("CALLED TWICE: InputStreamDataSource.getInputStream()");
        return new BufferedInputStream(inputStream);
        //return new ByteArrayInputStream("THIS 'NEW' INPUT STREAM WORKS BUT 'EXISTING' INPUT STREAM RESULTS IN ZERO-BYTE ATTACHMENT".getBytes());
    }

    public OutputStream getOutputStream() throws IOException
    {
        throw new UnsupportedOperationException("Not implemented");
    }
}

DataSource 提供方法getInputStream() 来获取电子邮件附件内容的InputStream

如果我返回一个不依赖于“现有”InputStream 的“新”InputStream,那么它可以正常工作。但是,如果我返回一个“现有的”InputStream,那么电子邮件消息会以一个零字节的附件传递。

是否可以使用javax.mail 发送电子邮件,并使用“现有”InputStream 作为电子邮件附件内容?

【问题讨论】:

    标签: java jakarta-mail javax.activation


    【解决方案1】:

    当前的 java 邮件实现会检查输入流两次:第一次检测数据的编码,第二次发送数据。

    如果您使用 EncodingAware 接口指定编码,则可以防止第一次通过。提供的 DataSource 应实现此接口。这是一个例子:

    public class AttachementDataSource implements javax.activation.DataSource, javax.mail.EncodingAware {
    
        private final InputStreamSource inputStreamSource; 
        
        public AttachementDataSource(InputStreamSource inputStreamSource) {
            this.inputStreamSource = inputStreamSource;
        }
        
        @Override
        public InputStream getInputStream() throws IOException {
            return inputStreamSource.getInputStream();
        }
    
        @Override
        public OutputStream getOutputStream() throws IOException {
            throw new UnsupportedOperationException("Read-only javax.activation.DataSource");
        }
    
        @Override
        public String getContentType() {
            return "application/octet-stream";
        }
    
        @Override
        public String getName() {
            return "inline";
        }
    
        @Override
        public String getEncoding() {
            return "base64";
        }
    }
    

    【讨论】:

      【解决方案2】:

      我重写了您的 InputStreamDataSource 类,它对我有用。

      class InputStreamDataSource implements DataSource {
          String contentType;
          String name;
      
          byte[] fileData;
      
          public InputStreamDataSource(String contentType, String name, InputStream inputStream) throws IOException {
              this.contentType = contentType;
              this.name = name;
              /**
               * It seems DataSource will close inputStream and reopen it.
               * I converted inputStream to a byte array, so it won't be closed again.
               */
              fileData = IOUtils.toByteArray(inputStream);
          }
      
          public String getContentType() {
              return contentType;
          }
      
          public String getName() {
              return name;
          }
      
          public InputStream getInputStream() throws IOException {
              /**
               * Convert byte array back to inputStream.
               */
              return new ByteArrayInputStream(fileData);
          }
      
          public OutputStream getOutputStream() throws IOException {
              throw new UnsupportedOperationException("Not implemented");
          }
      }
      

      【讨论】:

      • 看来 DataSource 会关闭您的 inputStream 并重新打开它。所以我将 inputStream 转换为字节数组以避免它。
      【解决方案3】:

      我使用此代码发送带有网络下载附件的电子邮件。您可以根据自己的目的轻松编辑它。在 mimeType 中使用附件的 mime 类型。编码愉快。

      try {
      
              Message message = new MimeMessage(session);
              message.setFrom(new InternetAddress(
                      "sender@gmail.com"));
              message.setRecipients(Message.RecipientType.TO,
                      InternetAddress.parse("reciever@gmail.com"));
              message.setSubject("subject");
      
              Multipart multipart = new MimeMultipart();
      
              URL url = new URL(url);
      
              InputStream is = url.openStream();
              MimeBodyPart bodyPart = new MimeBodyPart(is);
      
              multipart.addBodyPart(bodyPart);
      
              message.setContent(multipart);
              message.addHeader("Content-Type", mimeType);
              Transport.send(message);
              logger.info("SENT to" + message.getRecipients(RecipientType.TO));
      
          } catch (MessagingException e) {
              //some implementation
          }
      

      【讨论】:

      • 请添加一些文档/说明。
      • 我尝试使用此代码发送带有网络下载附件的电子邮件。您可以根据自己的目的轻松编辑它。在 mimeType 中使用附件的 mime 类型。编码愉快。
      【解决方案4】:

      如果 InputStream 包含 mime 标头,则使用 javax.mail.internet.MimeBodyPart(InputStream) 构造函数。您不需要使用自定义的 DataSource 类。

      否则,如果InputStream 只是没有标头的主体,则将流转换为字节数组并使用javax.mail.internet.MimeBodyPart(InternetHeaders, byte[]) 构造函数来提供标头。

      【讨论】:

        【解决方案5】:

        我解决了将InputStream 转换为字节数组并将其转换为 Base64 格式的问题。

        //get file name
        String fileName = ...;
        //get content type
        String fileContentType = ...;
        //get file content
        InputStream fileStream = ...;
        
        //convert to byte array
        byte[] fileByteArray = IOUtils.toByteArray(fileStream);
        //and convert to Base64
        byte[] fileBase64ByteArray = java.util.Base64.getEncoder().encode(fileByteArray);
        
        //manually define headers
        InternetHeaders fileHeaders = new InternetHeaders();
        fileHeaders.setHeader("Content-Type", fileContentType + "; name=\"" + fileName + "\"");
        fileHeaders.setHeader("Content-Transfer-Encoding", "base64");
        fileHeaders.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        
        //build MIME body part
        MimeBodyPart mbp = new MimeBodyPart(fileHeaders, fileBase64ByteArray);
        mbp.setFileName(fileName);
        
        //add it to the multipart
        multipart.addBodyPart(mbp);
        

        【讨论】:

          【解决方案6】:

          编辑:

          https://community.oracle.com/thread/1590625

          TL;DR 使用ByteArrayDataSource


          必须深入研究 Oracle 的源代码...https://java.net/projects/javamail/sources/mercurial/content/mail/src/main/java/javax/mail/internet/MimeBodyPart.java

          当前的 java 邮件实现是输入流的 2 倍:

          1. 首先确定是否应该将标头“Content-Transfer-Encoding”设置为 7 位或 8 位(参见Content Transfer Encoding 7bit or 8 bit
          2. 然后第二次实际写入消息时

          ...哪一种很糟糕,因为 整个 流(可能通过慢速连接达到数百 MB)将被读取两次...对于“消费”一次阅读。


          我尝试的第一个“解决方法”是自己指定标题:

          attachmentPart.setDataHandler(new DataHandler(source));
          attachmentPart.setHeader("Content-Transfer-Encoding", "8bit");
          attachmentPart.setHeader("Content-Type", ds.getContentType() + "; " + ds.getName());
          

          ...按照这个顺序,而不是相反...因为出于某种原因setDataHandler 在内部调用另一个方法invalidateContentHeaders,它再次清除"Content-Transfer-Encoding" 标头(wtf?!)

          听起来不错,邮件已发送,万岁!!! :D ... :( 见下一个


          附件发送...但损坏

          我的邮件服务器中收到的文件已损坏。嗯。 为什么?!。经过长时间的搜索并再次深入研究这个糟糕的 java 邮件代码,我找到了它,他们将 InputStream 传递到 LineOutputStream 中,这会改变 binary 数据的行尾。嗯。 java邮件实现真的是一团糟。 :/

          【讨论】:

            猜你喜欢
            • 2015-09-09
            • 1970-01-01
            • 2011-08-06
            • 2014-07-13
            • 2020-09-16
            • 1970-01-01
            相关资源
            最近更新 更多