Отправить письмо с 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 раза по входному потоку:
- Сначала необходимо определить, должен ли заголовок "Content-Transfer-Encoding" установить значение 7 или 8 бит (см. " Кодирование передачи содержимого 7-бит или 8-бит").
- Затем второй раз, когда он действительно пишет сообщение
... какой отстой, потому что весь поток (возможно, сотни МБ по медленному соединению) будет прочитан два раза... и приводит именно к этой проблеме для потоков, которые "потребляются" после чтения.
Первый "обходной путь", который я попробовал, состоит в том, чтобы указать заголовки самостоятельно:
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";
}
}