Отправить письмо с javax.mail, используя существующий InputStream в качестве содержимого вложения

Можно ли отправить электронное письмо, используя 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 для содержимого вложения сообщения электронной почты?

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 бит (см. " Кодирование передачи содержимого 7-бит или 8-бит").
  2. Затем второй раз, когда он действительно пишет сообщение

... какой отстой, потому что весь поток (возможно, сотни МБ по медленному соединению) будет прочитан два раза... и приводит именно к этой проблеме для потоков, которые "потребляются" после чтения.


Первый "обходной путь", который я попробовал, состоит в том, чтобы указать заголовки самостоятельно:

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...:(смотрите дальше


Приложение отправлено... но сломано

Полученный файл на моем почтовом сервере не работает. Да. Зачем?!, После долгих поисков и копания в этом дерьмовом почтовом коде я обнаружил, что они отправляют InputStream в LineOutputStream который изменяет окончания строк ваших двоичных данных. Мех. Реализация Java-почты действительно беспорядок.:/

Я переписал ваш класс 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");
}

}

Я решил это преобразовать 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);

Если InputStream содержит заголовки MIME, затем используйте javax.mail.internet.MimeBodyPart(InputStream) конструктор. Вам не нужно использовать обычай DataSource учебный класс.

В противном случае, если InputStream это просто тело без заголовков, затем преобразовать поток в байтовый массив и использовать javax.mail.internet.MimeBodyPart(InternetHeaders, byte[]) конструктор для предоставления ваших заголовков.

Я использую этот код для отправки электронной почты с приложением, загруженным через Интернет. Вы можете легко редактировать его для своих целей. В 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
    }

Текущая реализация электронной почты Java дважды обрабатывает входной поток: первый проход для определения кодировки данных, а второй — для отправки данных.

Вы можете предотвратить первый проход, если укажете кодировку с помощью интерфейса EncodingAware. Предоставленный источник данных должен реализовать этот интерфейс. Вот пример:

      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";
    }
}
Другие вопросы по тегам