package com.mailserver.imap; import com.mailserver.domain.Attachment; import com.mailserver.domain.Message; import jakarta.activation.DataHandler; import jakarta.activation.FileDataSource; import jakarta.mail.MessagingException; import jakarta.mail.Session; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeBodyPart; import jakarta.mail.internet.MimeMessage; import jakarta.mail.internet.MimeMultipart; import jakarta.mail.internet.MimeUtility; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; /** * MIME text of a message that has no original (written in the web client): built from the stored fields and * attachment files. The boundary and Message-ID come from the message id, so the result is the same every time. */ public final class MimeSource { private MimeSource() { } public static byte[] build(Message message, List attachments, String hostname) throws MessagingException, IOException { Properties props = new Properties(); props.put("mail.mime.splitlongparameters", "true"); Session session = Session.getInstance(props); String messageId = message.getMessageId() == null || message.getMessageId().isBlank() ? message.getId() + "@" + hostname : message.getMessageId().replaceAll("^<|>$", ""); if (!messageId.contains("@")) { messageId = messageId + "@" + hostname; } String finalMessageId = "<" + messageId + ">"; MimeMessage mime = new MimeMessage(session) { @Override protected void updateMessageID() throws MessagingException { setHeader("Message-ID", finalMessageId); } }; String from = message.getSenderEmail() == null ? "unknown@" + hostname : message.getSenderEmail(); mime.setFrom(new InternetAddress(from, message.getSenderName(), "UTF-8")); List to = addresses(message.getRecipientEmail()); if (!to.isEmpty()) { mime.setRecipients(jakarta.mail.Message.RecipientType.TO, to.toArray(new InternetAddress[0])); } List cc = addresses(message.getCcEmail()); if (!cc.isEmpty()) { mime.setRecipients(jakarta.mail.Message.RecipientType.CC, cc.toArray(new InternetAddress[0])); } mime.setSubject(message.getSubject() == null ? "" : message.getSubject(), "UTF-8"); if (message.getReceivedAt() != null) { mime.setSentDate(Date.from(message.getReceivedAt())); } String body = message.getBody() == null ? "" : message.getBody(); String subtype = message.isHtml() ? "html" : "plain"; if (attachments.isEmpty()) { mime.setText(body, "UTF-8", subtype); } else { MimeMultipart mixed = new FixedBoundaryMultipart("mixed", "=_rms_" + message.getId()); MimeBodyPart text = new MimeBodyPart(); text.setText(body, "UTF-8", subtype); mixed.addBodyPart(text); for (Attachment attachment : attachments) { Path file = Path.of(attachment.getFilePath()); if (!Files.isRegularFile(file)) { continue; } MimeBodyPart part = new MimeBodyPart(); FileDataSource source = new FileDataSource(file.toFile()) { @Override public String getContentType() { return attachment.getContentType() == null ? "application/octet-stream" : attachment.getContentType(); } }; part.setDataHandler(new DataHandler(source)); part.setFileName(MimeUtility.encodeText(attachment.getFilename(), "UTF-8", null)); part.setDisposition(attachment.getContentId() != null ? MimeBodyPart.INLINE : MimeBodyPart.ATTACHMENT); if (attachment.getContentId() != null) { part.setContentID("<" + attachment.getContentId().replaceAll("^<|>$", "") + ">"); } mixed.addBodyPart(part); } mime.setContent(mixed); } mime.saveChanges(); ByteArrayOutputStream out = new ByteArrayOutputStream(); mime.writeTo(out); return out.toByteArray(); } private static List addresses(String list) { List result = new ArrayList<>(); if (list == null || list.isBlank()) { return result; } for (String one : list.split("[,;]")) { String address = one.trim(); if (!address.isEmpty()) { try { result.add(new InternetAddress(address)); } catch (MessagingException ignored) { // a broken address is left out, the message is still served } } } return result; } /** MimeMultipart with a given boundary instead of a random one. */ private static final class FixedBoundaryMultipart extends MimeMultipart { FixedBoundaryMultipart(String subtype, String boundary) { super(subtype); this.contentType = "multipart/" + subtype + "; boundary=\"" + boundary + "\""; } } }