Reputation: 189
Is it possible to send an email using javax.mail
and using an “existing” InputStream
for the email message attachment content?
Currently I am building the email message as follows:
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
is implemented as follows:
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");
}
}
The DataSource
provides method getInputStream()
to get the InputStream
for the email message attachment content.
If I return a "new" InputStream
which does not depend on an "existing" InputStream
then it works fine. But if I return an “existing” InputStream then the email message is delivered with a zero-byte attachment.
Is it possible to send an email using javax.mail
, and use an “existing” InputStream
for the email message attachment content?
Upvotes: 14
Views: 18365
Reputation: 11
The current java mail implementation goes over the input stream twice: the first pass to detect the encoding for the data and the second one to send the data.
You can prevent the first pass if you specify the encoding using the EncodingAware interface. The supplied DataSource should implement this interface. Here is an example:
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";
}
}
Upvotes: 0
Reputation: 31
I rewrited your InputStreamDataSource class, and it works for me.
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");
}
}
Upvotes: 3
Reputation: 393
I use this code for sending email with web downloaded attachment. You can easily edit it for your purpose. In mimeType use mime type of your attachment. Happy coding.
try {
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(
"[email protected]"));
message.setRecipients(Message.RecipientType.TO,
InternetAddress.parse("[email protected]"));
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
}
Upvotes: 0
Reputation: 11045
If the InputStream
contains mime headers then use the javax.mail.internet.MimeBodyPart(InputStream)
constructor. You don't need to use a custom DataSource
class.
Otherwise, if the InputStream
is just the body without headers then convert the stream into a byte array and use the javax.mail.internet.MimeBodyPart(InternetHeaders, byte[])
constructor to provide your headers.
Upvotes: 2
Reputation: 27380
I solved it converting the InputStream
to a byte array and converting it to Base64 format.
//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);
Upvotes: 3
Reputation: 5329
EDIT:
see https://community.oracle.com/thread/1590625
TL;DR use a ByteArrayDataSource
One has to delve into Oracle's source code... https://java.net/projects/javamail/sources/mercurial/content/mail/src/main/java/javax/mail/internet/MimeBodyPart.java
The current java mail implementation goes 2 times over the input stream:
...which kind of sucks because the whole stream (maybe hundreds of MB over a slow connection) will be read two times ...and leads to exactly this issue for streams that are "consumed" once read.
The first "workaround" I tried is to specify the headers yourself:
attachmentPart.setDataHandler(new DataHandler(source));
attachmentPart.setHeader("Content-Transfer-Encoding", "8bit");
attachmentPart.setHeader("Content-Type", ds.getContentType() + "; " + ds.getName());
...and in that order, and not the other way round ...because for some reason setDataHandler
calls internally another method invalidateContentHeaders
which clears the "Content-Transfer-Encoding"
header again (wtf?!)
Sounded great, the mail was sent, hooray!!! :D ... :( see next
Attachment send ...but broken
The received file in my mail server is broken. Huh. Why?!. After a long search and delving again in this crappy java mail code, I found it, they pipe the InputStream
into a LineOutputStream
which changes the line endings of your binary data. Meh. The java mail implementation is really a mess. :/
Upvotes: 4