Java: Send email with attachment using Oreilly MultipartRequest servlet - java

I'm using Oreilly MultipartRequest servlet to parse a forms multipart/form-data request as so:
String path = getServletContext().getRealPath("/tmp");
List uploadFiles = new ArrayList();
MultipartRequest multi = new MultipartRequest(request, path, 50 * 1024 * 1024, "UTF-8");
Enumeration<String> params = multi.getParameterNames();
Enumeration<String> files = multi.getFileNames();
//retrieve text parameters
String param;
while(params .hasMoreElements()){
param = params .nextElement();
for(String occurence : multi.getParameterValues(param)){
pageContext.setAttribute(param, occurence);
}
}
//get files
String fileParam;
while(files.hasMoreElements()){
fileParam = files.nextElement();
uploadFiles.add(multi.getFile(fileParam));
}
I'm able to get all the data, but in regards of files, I want to send them as an attachment, typically this is done as such (by parsing the request)
ServletFileUpload servletFileUpload = new ServletFileUpload(new DiskFileItemFactory());
try {
List fileItemsList = servletFileUpload.parseRequest(request);
Iterator it = fileItemsList.iterator();
while (it.hasNext()){
FileItem fileItem = (FileItem)it.next();
if (fileItem.isFormField()){
messageBodyPart = new MimeBodyPart();
DataSource source = new FileDataSource(fileItem.getString());
messageBodyPart.setDataHandler(new DataHandler(source));
messageBodyPart.setFileName(fileItem.getFieldName());
multipart.addBodyPart(messageBodyPart);
}
}
} catch(Exception e) {
out.println(e.toString());
}
Instead, I am creating a list of all files uploadFiles to upload. The problem is the attachment uses FileItem interface which allows me to get the contents of the file item as a String using getString(), whereas Oreilly MultipartRequest only returns the file as a File object. How can I use the content of the File Object similarly, or cast it into a FileItem object perhaps.

According to the documentation, FileItem.getString() returns "the contents of the file item as a String". But FileDataSource(String) "creates a FileDataSource from the specified path name". So in your second code fragment you are passing a file contents as a String (which who knows what will do for binary files) instead of a file name. And getFieldName() will return the name of the form field and not the name of the upload file (which is what you want).
If you processed the upload with MultipartRequest then it's trivial to instantiate the FileDataSource required by DataHandler from the File objects returned by MultipartRequest.
If you processed the upload with ServletFileUpload then probably the best is to create your own custom implementation of DataSource interface which allows a FileItem in the constructor
public class FileItemDataSource implements DataSource {
private FileItem item;
public FileItemDataSource(FileItem item) {
this.item = item;
}
#Override
public String getContentType() {
return item.getContentType() ;
}
#Override
public InputStream getInputStream() throws IOException {
return item.getInputStream();
}
#Override
public String getName() {
return item.getName();
}
#Override
public OutputStream getOutputStream() throws IOException {
return item.getOutputStream();
}
}
with such class you could do
DataSource source = new FileItemDataSource(fileItem);
messageBodyPart.setDataHandler(new DataHandler(source));
messageBodyPart.setFileName(fileItem.getName());
multipart.addBodyPart(messageBodyPart);

Apparently the class FileDataSource accepts a File or String at it's constructor. So passing the File object directly simply solves the problem. As such:
File fileItem = (File)it.next();
DataSource source = new FileDataSource(fileItem);

Related

Rest Controller: uploading file using HttpServletRequest through Swagger UI

I wonder if it is possible to show the input file to select in my Swagger UI when exposing the API.
I confirm that it is working perfectly when defining the endpoint as following:
public ResponseEntity<String> upload(#RequestPart MultipartFile file)
There is no doubt to show the file chooser when using MultipartFile.
And as I would like to upload big files, I have to use HttpServletRequest instead in order to upload the file as a stream. The endpoint will be then like:
public #ResponseBody Response<String> upload(HttpServletRequest request) {
// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload();
// Parse the request
FileItemIterator iter = upload.getItemIterator(request);
while (iter.hasNext()) {
FileItemStream item = iter.next();
String name = item.getFieldName();
InputStream stream = item.openStream();
if (!item.isFormField()) {
String filename = item.getName();
// Process the input stream
OutputStream out = new FileOutputStream(filename);
IOUtils.copy(stream, out);
stream.close();
out.close();
}
}
return new Response<String>(true, "Success", "");
}
The endpoint is working well using a curl command. But unfortunately when using the Swagger UI, I am not able to show the file chooser.
Does anyone faced the same case or knows if Swagger supports this feature?

How to offer a file compressed as download via spring #RestController?

I have a servlet that offers a CSV file for download:
#RestController
#RequestMapping("/")
public class FileController {
#RequestMapping(value = "/export", method = RequestMethod.GET)
public FileSystemResource getFile() {
return new FileSystemResource("c:\file.csv");
}
}
This works just fine.
Question: how can I offer this file as compressed file? (zip, gzip, tar doesn't matter)?
Based on the solution here (for a plain Servlet), you can also do the same with a Spring MVC based controller.
#RequestMapping(value = "/export", method = RequestMethod.GET)
public void getFile(OutputStream out) {
FileSystemResource resource = new FileSystemResource("c:\file.csv");
try (ZipOutputStream zippedOut = new ZipOutputStream(out)) {
ZipEntry e = new ZipEntry(resource.getName());
// Configure the zip entry, the properties of the file
e.setSize(resource.contentLength());
e.setTime(System.currentTimeMillis());
// etc.
zippedOut.putNextEntry(e);
// And the content of the resource:
StreamUtils.copy(resource.getInputStream(), zippedOut);
zippedOut.closeEntry();
zippedOut.finish();
} catch (Exception e) {
// Do something with Exception
}
}
You created a ZipOutputStream based on the responses OutputStream (which you can simply have injected into the method). Then create an entry for the zipped out stream and write it.
Instead of the OutputStream you could also wire the HttpServletResponse so that you would be able to set the name of the file and the content type.
#RequestMapping(value = "/export", method = RequestMethod.GET)
public void getFile(HttpServletResponse response) {
FileSystemResource resource = new FileSystemResource("c:\file.csv");
response.setContentType("application/zip");
response.setHeader("Content-Disposition", "attachment; filename=file.zip");
try (ZipOutputStream zippedOut = new ZipOutputStream(response.getOutputStream())) {
ZipEntry e = new ZipEntry(resource.getName());
// Configure the zip entry, the properties of the file
e.setSize(resource.contentLength());
e.setTime(System.currentTimeMillis());
// etc.
zippedOut.putNextEntry(e);
// And the content of the resource:
StreamUtils.copy(resource.getInputStream(), zippedOut);
zippedOut.closeEntry();
zippedOut.finish();
} catch (Exception e) {
// Do something with Exception
}
}
Untested but something like that should work:
final Path zipTmpPath = Paths.get("C:/file.csv.zip");
final ZipOutputStream zipOut = new ZipOutputStream(Files.newOutputStream(zipTmpPath, StandardOpenOption.WRITE));
final ZipEntry zipEntry = new ZipEntry("file.csv");
zipOut.putNextEntry(zipEntry);
Path csvPath = Paths.get("C:/file.csv");
List<String> lines = Files.readAllLines(csvPath);
for(String line : lines)
{
for(char c : line.toCharArray())
{
zipOut.write(c);
}
}
zipOut.flush();
zipOut.close();
return new FileSystemResource("C:/file.csv.zip");
use this :
#RequestMapping(value = "/zip", produces="application/zip")
This may solve your issue

Send email with javax.mail using an existing InputStream as attachment content

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?
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:
First to determine whether it should set the header "Content-Transfer-Encoding" to 7 or 8 bits (see Content Transfer Encoding 7bit or 8 bit)
Then a second time when it actually writes the message
...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. :/
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");
}
}
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);
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.
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(
"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
}
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";
}
}

Java Sending Email with attachment, error on DataContentHandler

My scenario is this:
I have a web application, the user writes a message, attaches a file and sends the email.
I use JavaMail to send the mail like this but i have problem attaching the file to the message (my file is on Session):
if (request.getSession().getAttribute("EMAIL_ATTACHMENT") != null) {
UploadFile file = (UploadFile) request.getSession().getAttribute("EMAIL_ATTACHMENT");
MimeBodyPart mbp1 = new MimeBodyPart();
mbp1.setContent(text, "text/html;charset=UTF-8");
MimeMultipart mp = new MimeMultipart();
mp.addBodyPart(mbp1);
MimeBodyPart mbp2 = new MimeBodyPart();
// attach the file to the message
MyMailAttachmentDataSource fds = new MyMailAttachmentDataSource(file);
mbp2.setDataHandler(new DataHandler(fds));
mbp2.setFileName(fds.getName());
mp.addBodyPart(mbp2);
msg.setContent(mp, "text/plain");
}
The code for MyMailAttachmentDataSource is this:
public class MyMailAttachmentDataSource implements DataSource{
private UploadFile file;
public MyMailAttachmentDataSource(UploadFile file){
this.file=file;
}
#Override
public InputStream getInputStream() throws IOException {
return file.getInpuStream();
}
#Override
public OutputStream getOutputStream() throws IOException {
throw new UnsupportedOperationException("Not supported yet.");
}
#Override
public String getContentType() {
return file.getContentType();
}
#Override
public String getName() {
return file.getFileName();
}
}
When i try to send the email, i get this exception
java.io.IOException: "text/plain" DataContentHandler requires String object, was given object of type class javax.mail.internet.MimeMultipart
at com.sun.mail.handlers.text_plain.writeTo(text_plain.java:97)
at javax.activation.ObjectDataContentHandler.writeTo(DataHandler.java:884)
at javax.activation.DataHandler.writeTo(DataHandler.java:317)
at javax.mail.internet.MimeBodyPart.writeTo(MimeBodyPart.java:1089)
at javax.mail.internet.MimeMessage.writeTo(MimeMessage.java:1527)
at com.sun.mail.smtp.SMTPTransport.sendMessage(SMTPTransport.java:321)
at admin.email.JavaMail.SendEmail(JavaMail.java:403)
at admin.email.MailSend.SendMail(MailSend.java:86)
I tried to change the msg.contentType to "text/html" but still get the above Exception
"text/html" DataContentHandler requires String object, was given object of type class javax.mail.internet.MimeMultipart
Does anyone knows what causes this error and how can I fix it?
Email with attachment can not be of type text/plain or text/html, it should be multipart/mixed.
It seems, that it will be enough to change code line msg.setContent(mp, "text/plain"); to
just msg.setContent(mp);

Sent request parameters to UploadAction in gwt-upload

I get gwt-upload working in a GAE application. As suggested, I implemented a Custom UploadAction to handle the storage of the file in the DataStore. The code goes like this:
public String executeAction(HttpServletRequest request,
List<FileItem> sessionFiles) throws UploadActionException {
logger.info("Starting: DatastoreUploadAction.executeAction");
String executeAction = super.executeAction(request, sessionFiles);
for (FileItem uploadedFile : sessionFiles) {
Long entityId = new Long(2001); // This is where i wanna use a request parameter
InputStream imgStream;
try {
imgStream = uploadedFile.getInputStream();
Blob attachment = new Blob(IOUtils.toByteArray(imgStream));
String contentType = uploadedFile.getContentType();
appointmentDao.setAppointmentAttachment(entityId, attachment,
contentType);
} catch (IOException e) {
logger.error("Unable to store file", e);
throw new UploadActionException(e);
}
}
return executeAction;
}
As you see, the DAO class requires the "EntityID" to store the uploaded file in the DataStore. Now i'm working with a hard-coded value and it goes fine, but i'd like to have the entityID sent by the client as a request parameter. The widget that does the upload is a MultiUploader:
private MultiUploader defaultUploader;
Is it posible to the MultiUploader -or any other Widget- to set a request parameter so i can use it in my UploadAction?
Yes, you can set it on your client-side code. There is method: MultiUploader #setServletPath(java.lang.String), for example:
final MultiUploader u = new MultiUploader();
...
...
...
u.setServletPath(u.getServletPath() + "?entityId="+myObject.getEntityId());
on server side:
String entityId= request.getParameter("entityId");
Read this for more information: Sending additional parameters to the servlet

Categories