SusiMail: Add folders, drafts, background sending (ticket #2087)

Use with caution; cleanups and CSS to follow
This commit is contained in:
zzz
2018-04-14 15:50:07 +00:00
parent ffad52e48c
commit 844977cca3
13 changed files with 1238 additions and 283 deletions

View File

@@ -86,6 +86,6 @@ public class MemoryBuffer implements Buffer {
@Override
public String toString() {
return "SB " + (content == null ? "empty" : content.length + " bytes");
return "MB " + (content == null ? "empty" : content.length + " bytes");
}
}

View File

@@ -72,6 +72,14 @@ public class Attachment {
return new FileInputStream(data);
}
/**
* @return absolute path to the data file
* @since 0.9.35
*/
public String getPath() {
return data.getAbsolutePath();
}
/**
* The unencoded size
* @since 0.9.33

View File

@@ -0,0 +1,136 @@
package i2p.susi.webmail;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import i2p.susi.util.Buffer;
import i2p.susi.webmail.encoding.Encoding;
import i2p.susi.webmail.encoding.EncodingException;
import i2p.susi.webmail.encoding.EncodingFactory;
/**
* Holds a draft message and reference to attachments, if any
*
* Differences from Mail:
* - Never multipart, body is always text/plain UTF-8
* - Attachments are just headers containing name, type, encoding, and path to file
* - Bcc is a header
*
* @since 0.9.35
*/
class Draft extends Mail {
private final List<Attachment> attachments;
String[] bcc; // addresses only, enclosed by <>
private static final String HDR_ATTACH = "X-I2P-Attachment: ";
public static final String HDR_BCC = "Bcc: ";
public Draft(String uidl) {
super(uidl);
attachments = new ArrayList<Attachment>(4);
}
/**
* Overridden to process attachment and Bcc headers
*/
@Override
public synchronized void setBody(Buffer rb) {
super.setBody(rb);
MailPart part = getPart();
if (part != null) {
String[] hdrs = part.headerLines;
for (int i = 0; i < hdrs.length; i++) {
String hdr = hdrs[i];
if (hdr.startsWith(HDR_BCC)) {
ArrayList<String> list = new ArrayList<String>();
getRecipientsFromList(list, hdr.substring(HDR_BCC.length()).trim(), true);
if (list.isEmpty()) {
// don't set
} else if (bcc == null) {
bcc = list.toArray(new String[list.size()]);
} else {
// add to the array, shouldn't happen
for (int j = 0; j < bcc.length; j++) {
list.add(j, bcc[i]);
}
bcc = list.toArray(new String[list.size()]);
}
}
if (!hdr.startsWith(HDR_ATTACH))
break;
String[] flds = DataHelper.split(hdr.substring(HDR_ATTACH.length()), ",", 4);
if (flds.length != 4)
continue;
byte[] b = Base64.decode(flds[0]);
if (b == null)
continue;
String name = DataHelper.getUTF8(b);
String type = flds[1];
String enc = flds[2];
b = Base64.decode(flds[0]);
if (b == null)
continue;
String path = DataHelper.getUTF8(b);
attachments.add(new Attachment(name, type, enc, new File(path)));
}
}
}
@Override
public synchronized boolean hasAttachment() {
return !attachments.isEmpty();
}
/** @return may be null */
public synchronized String[] getBcc() {
return bcc;
}
/** @return non-null, not a copy */
public synchronized List<Attachment> getAttachments() {
return attachments;
}
public synchronized int addAttachment(Attachment a) {
int rv = attachments.indexOf(a);
if (rv >= 0)
return rv;
rv = attachments.size();
attachments.add(a);
return rv;
}
public synchronized void removeAttachment(int index) {
if (index >= 0 && index < attachments.size()) {
Attachment a = attachments.get(index);
a.deleteData();
attachments.remove(index);
}
}
public synchronized void clearAttachments() {
for (Attachment a : attachments) {
a.deleteData();
}
attachments.clear();
}
public synchronized StringBuilder encodeAttachments() {
StringBuilder buf = new StringBuilder(256 * attachments.size());
if (attachments.isEmpty())
return buf;
for (int i = 0; i < attachments.size(); i++) {
Attachment a = attachments.get(i);
buf.append(HDR_ATTACH);
buf.append(Base64.encode(a.getFileName())).append(',');
buf.append(a.getContentType()).append(',');
buf.append(a.getTransferEncoding()).append(',');
buf.append(Base64.encode(a.getPath())).append("\r\n");
}
return buf;
}
}

View File

@@ -300,6 +300,23 @@ class Mail {
{
if( text != null && text.length() > 0 ) {
String[] ccs = DataHelper.split(text, ",");
ok = getRecipientsFromList(recipients, ccs, ok);
}
return ok;
}
/**
* A little misnamed. Adds all addresses from the elements
* in text to the recipients list.
*
* @param recipients out param
* @param ok will be returned
* @return true if ALL e-mail addresses are valid AND the in parameter was true
* @since 0.9.35
*/
public static boolean getRecipientsFromList( ArrayList<String> recipients, String[] ccs, boolean ok )
{
if (ccs != null && ccs.length > 0 ) {
for( int i = 0; i < ccs.length; i++ ) {
String recipient = ccs[i].trim();
if( validateAddress( recipient ) ) {

View File

@@ -27,8 +27,11 @@ import i2p.susi.debug.Debug;
import i2p.susi.util.Config;
import i2p.susi.util.Buffer;
import i2p.susi.util.FileBuffer;
import i2p.susi.util.Folder;
import i2p.susi.util.Folder.SortOrder;
import i2p.susi.util.ReadBuffer;
import i2p.susi.util.MemoryBuffer;
import static i2p.susi.webmail.Sorters.*;
import i2p.susi.webmail.pop3.POP3MailBox;
import i2p.susi.webmail.pop3.POP3MailBox.FetchRequest;
@@ -43,9 +46,13 @@ import java.util.List;
import java.util.Set;
import net.i2p.I2PAppContext;
import net.i2p.util.FileUtil;
import net.i2p.util.I2PAppThread;
/**
* There's one of these for each Folder.
* However, only DIR_FOLDER has a non-null POP3MailBox.
*
* @author user
*/
class MailCache {
@@ -58,7 +65,11 @@ class MailCache {
private final Hashtable<String, Mail> mails;
private final PersistentMailCache disk;
private final I2PAppContext _context;
private final Folder<String> folder;
private final String folderName;
private NewMailListener _loadInProgress;
private boolean _isLoaded;
private final boolean _isDrafts;
/** Includes header, headers are generally 1KB to 1.5 KB,
* and bodies will compress well.
@@ -69,15 +80,156 @@ class MailCache {
/**
* Does NOT load the mails in. Caller MUST call loadFromDisk().
*
* @param mailbox non-null
* @param mailbox non-null for DIR_FOLDER; null otherwise
*/
MailCache(I2PAppContext ctx, POP3MailBox mailbox,
MailCache(I2PAppContext ctx, POP3MailBox mailbox, String folderName,
String host, int port, String user, String pass) throws IOException {
this.mailbox = mailbox;
mails = new Hashtable<String, Mail>();
disk = new PersistentMailCache(ctx, host, port, user, pass, PersistentMailCache.DIR_FOLDER);
disk = new PersistentMailCache(ctx, host, port, user, pass, folderName);
// TODO Drafts, Sent, Trash
_context = ctx;
Folder<String> folder = new Folder<String>();
// setElements() sorts, so configure the sorting first
//sessionObject.folder.addSorter( SORT_ID, new IDSorter( sessionObject.mailCache ) );
folder.addSorter(WebMail.SORT_SENDER, new SenderSorter(this));
folder.addSorter(WebMail.SORT_SUBJECT, new SubjectSorter(this));
folder.addSorter(WebMail.SORT_DATE, new DateSorter(this));
folder.addSorter(WebMail.SORT_SIZE, new SizeSorter(this));
// reverse sort, latest mail first
// TODO get user defaults from config
folder.setSortBy(WebMail.SORT_DEFAULT, WebMail.SORT_ORDER_DEFAULT);
this.folder = folder;
this.folderName = folderName;
_isDrafts = folderName.equals(WebMail.DIR_DRAFTS);
}
/**
* @since 0.9.35
* @return as passed in
*/
public String getFolderName() {
return folderName;
}
/**
* @since 0.9.35
* @return translation of name passed in
*/
public String getTranslatedName() {
String rv = folderName.equals(WebMail.DIR_FOLDER) ? "Inbox" : folderName;
return Messages.getString(rv);
}
/**
* @since 0.9.35
* @return non-null
*/
public Folder<String> getFolder() {
return folder;
}
/**
* For writing a new full mail (NOT headers only)
* Caller must close.
* @since 0.9.35
*/
public Buffer getFullWriteBuffer(String uidl) {
// no locking this way
return disk.getFullBuffer(uidl);
}
/**
* For writing a new full mail
* @param buffer as received from getFullBuffer
* @since 0.9.35
*/
public void writeComplete(String uidl, Buffer buffer, boolean success) {
buffer.writeComplete(success);
if (success) {
Mail mail;
if (_isDrafts)
mail = new Draft(uidl);
else
mail = new Mail(uidl);
mail.setBody(buffer);
synchronized(mails) {
mails.put(uidl, mail);
}
folder.addElement(uidl);
}
}
/**
* @return non-null only for Drafts
* @since 0.9.35
*/
public File getAttachmentDir() {
return disk.getAttachmentDir();
}
/**
* Move a mail to another MailCache, neither may be DIR_DRAFTS
* @return success
* @since 0.9.35
*/
public boolean moveTo(String uidl, MailCache toMC) {
if (folderName.equals(WebMail.DIR_DRAFTS) ||
toMC.getFolderName().equals(WebMail.DIR_DRAFTS))
return false;
Mail mail;
synchronized(mails) {
mail = mails.get(uidl);
if (mail == null)
return false;
if (!mail.hasBody())
return false;
File from = disk.getFullFile(uidl);
if (!from.exists())
return false;
PersistentMailCache toPMC = toMC.disk;
File to = toPMC.getFullFile(uidl);
if (to.exists())
return false;
if (!FileUtil.rename(from, to))
return false;
mails.remove(uidl);
folder.removeElement(uidl);
}
toMC.movedTo(mail);
if (mailbox != null)
mailbox.queueForDeletion(mail.uidl);
return true;
}
/**
* Moved a mail from another MailCache
* @since 0.9.35
*/
private void movedTo(Mail mail) {
synchronized(mails) {
// we must reset the body of the mail to the new FileBuffer
Buffer body = disk.getFullBuffer(mail.uidl);
mail.setBody(body);
mails.put(mail.uidl, mail);
}
folder.addElement(mail.uidl);
}
/**
* Is loadFromDisk in progress?
* @since 0.9.35
*/
public synchronized boolean isLoading() {
return _loadInProgress != null;
}
/**
* Has loadFromDisk completed?
* @since 0.9.35
*/
public synchronized boolean isLoaded() {
return _isLoaded;
}
/**
@@ -88,8 +240,9 @@ class MailCache {
* @since 0.9.13
*/
public synchronized boolean loadFromDisk(NewMailListener nml) {
if (_loadInProgress != null)
if (_isLoaded || _loadInProgress != null)
return false;
Debug.debug(Debug.DEBUG, "Loading folder " + folderName);
Thread t = new I2PAppThread(new LoadMailRunner(nml), "Email loader");
_loadInProgress = nml;
try {
@@ -115,10 +268,12 @@ class MailCache {
blockingLoadFromDisk();
if(!mails.isEmpty())
result = true;
Debug.debug(Debug.DEBUG, "Folder loaded: " + folderName);
} finally {
synchronized(MailCache.this) {
if (_loadInProgress == _nml)
_loadInProgress = null;
_isLoaded = true;
}
_nml.foundNewMail(result);
}
@@ -133,7 +288,7 @@ class MailCache {
*/
private void blockingLoadFromDisk() {
synchronized(mails) {
if (!mails.isEmpty())
if (_isLoaded)
throw new IllegalStateException();
Collection<Mail> dmails = disk.getMails();
for (Mail mail : dmails) {
@@ -164,7 +319,8 @@ class MailCache {
}
/**
* Fetch any needed data from pop3 server, unless mode is CACHE_ONLY.
* Fetch any needed data from pop3 server, unless mode is CACHE_ONLY,
* or this isn't the Inbox.
* Blocking unless mode is CACHE_ONLY.
*
* @param uidl message id to get
@@ -173,7 +329,6 @@ class MailCache {
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public Mail getMail(String uidl, FetchMode mode) {
Mail mail = null, newMail = null;
/*
@@ -182,6 +337,9 @@ class MailCache {
synchronized(mails) {
mail = mails.get( uidl );
if( mail == null ) {
// if not in inbox, we can't fetch, this is what we have
if (mailbox == null)
return null;
newMail = new Mail(uidl);
// TODO really?
mails.put( uidl, newMail );
@@ -193,13 +351,20 @@ class MailCache {
}
if (mail.markForDeletion)
return null;
// if not in inbox, we can't fetch, this is what we have
if (mailbox == null)
return mail;
long sz = mail.getSize();
if (mode == FetchMode.HEADER && sz > 0 && sz <= FETCH_ALL_SIZE)
mode = FetchMode.ALL;
if (mode == FetchMode.HEADER) {
if(!mail.hasHeader())
mail.setHeader(mailbox.getHeader(uidl));
if (!mail.hasHeader()) {
Buffer buf = mailbox.getHeader(uidl);
if (buf != null)
mail.setHeader(buf);
}
} else if (mode == FetchMode.ALL) {
if(!mail.hasBody()) {
File file = new File(_context.getTempDir(), "susimail-new-" + _context.random().nextLong());
@@ -223,6 +388,7 @@ class MailCache {
* Mail objects are inserted into the requests.
* After this, call getUIDLs() to get all known mail UIDLs.
* MUST already be connected, otherwise returns false.
* Call only on inbox!
*
* Blocking.
*
@@ -234,6 +400,10 @@ class MailCache {
public boolean getMail(FetchMode mode) {
if (mode == FetchMode.CACHE_ONLY)
throw new IllegalArgumentException();
if (mailbox == null) {
Debug.debug(Debug.DEBUG, "getMail() mode " + mode + " called on wrong folder " + getFolderName(), new Exception());
return false;
}
boolean hOnly = mode == FetchMode.HEADER;
Collection<String> popKnown = mailbox.getUIDLs();
@@ -348,6 +518,7 @@ class MailCache {
* Mark mail for deletion locally.
* Send delete requests to POP3 then quit and reconnect.
* No success/failure indication is returned.
* Does not delete from folder.
*
* @since 0.9.13
*/
@@ -359,6 +530,7 @@ class MailCache {
* Mark mail for deletion locally.
* Send delete requests to POP3 then quit and reconnect.
* No success/failure indication is returned.
* Does not delete from folder.
*
* @since 0.9.13
*/
@@ -381,7 +553,8 @@ class MailCache {
}
if (toDelete.isEmpty())
return;
mailbox.queueForDeletion(toDelete);
if (mailbox != null)
mailbox.queueForDeletion(toDelete);
}
/**

View File

@@ -301,10 +301,12 @@ class MailPart {
}
/**
* Synched because FileBuffer keeps stream open
*
* @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n
* @since 0.9.13
*/
public void decode(int offset, Buffer out) throws IOException {
public synchronized void decode(int offset, Buffer out) throws IOException {
String encg = encoding;
if (encg == null) {
//throw new DecodingException("No encoding specified");
@@ -319,9 +321,7 @@ class MailPart {
CountingOutputStream cos = null;
Buffer dout = null;
try {
in = buffer.getInputStream();
DataHelper.skip(in, buffer.getOffset() + beginBody + offset);
lin = new LimitInputStream(in, end - beginBody - offset);
lin = getRawInputStream(offset);
if (decodedLength < 0) {
cos = new CountingOutputStream(out.getOutputStream());
dout = new OutputStreamBuffer(cos);
@@ -341,7 +341,6 @@ class MailPart {
Debug.debug(Debug.DEBUG, "Decode IOE", ioe);
throw ioe;
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {};
if (lin != null) try { lin.close(); } catch (IOException ioe) {};
buffer.readComplete(true);
// let the servlet do this
@@ -354,6 +353,39 @@ class MailPart {
decodedLength = (int) cos.getWritten();
}
/**
* Synched because FileBuffer keeps stream open
* Caller must close out
*
* @since 0.9.35
*/
public synchronized void outputRaw(OutputStream out) throws IOException {
LimitInputStream lin = null;
try {
lin = getRawInputStream(0);
DataHelper.copy(lin, out);
} catch (IOException ioe) {
Debug.debug(Debug.DEBUG, "Decode IOE", ioe);
throw ioe;
} finally {
if (lin != null) try { lin.close(); } catch (IOException ioe) {};
buffer.readComplete(true);
}
}
/**
* Synched because FileBuffer keeps stream open
* Caller must call readComplete() on buffer
*
* @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n
* @since 0.9.35
*/
private synchronized LimitInputStream getRawInputStream(int offset) throws IOException {
InputStream in = buffer.getInputStream();
DataHelper.skip(in, buffer.getOffset() + beginBody + offset);
return new LimitInputStream(in, end - beginBody - offset);
}
private static String getFirstAttribute( String line )
{
String result = null;

View File

@@ -52,7 +52,9 @@ import net.i2p.util.SystemVersion;
* Exporting to a Maildir format would be just ungzipping
* each file to a flat directory.
*
* TODO draft and sent folders, cached server caps and config.
* This class should only be accessed from MailCache.
*
* TODO cached server caps and config.
*
* @since 0.9.14
*/
@@ -68,17 +70,16 @@ class PersistentMailCache {
private final Object _lock;
private final File _cacheDir;
// non-null only for Drafts
private final File _attachmentDir;
private final I2PAppContext _context;
private final boolean _isDrafts;
private static final String DIR_SUSI = "susimail";
private static final String DIR_CACHE = "cache";
private static final String CACHE_PREFIX = "cache-";
public static final String DIR_FOLDER = "cur"; // MailDir-like
public static final String DIR_DRAFTS = "Drafts"; // MailDir-like
public static final String DIR_SENT = "Sent"; // MailDir-like
public static final String DIR_TRASH = "Trash"; // MailDir-like
public static final String DIR_SPAM = "Bulk Mail"; // MailDir-like
public static final String DIR_IMPORT = "import"; // Flat with .eml files, debug only for now
public static final String DIR_ATTACHMENTS = "attachments"; // Flat with draft attachment files
private static final String DIR_PREFIX = "s";
private static final String FILE_PREFIX = "mail-";
private static final String HDR_SUFFIX = ".hdr.txt.gz";
@@ -91,16 +92,23 @@ class PersistentMailCache {
* Does NOT load the mails in. Caller MUST call getMails().
*
* @param pass ignored
* @param folder use DIR_FOLDER
* @param folder e.g. DIR_FOLDER
*/
public PersistentMailCache(I2PAppContext ctx, String host, int port, String user, String pass, String folder) throws IOException {
_context = ctx;
_isDrafts = folder.equals(WebMail.DIR_DRAFTS);
_lock = getLock(host, port, user, pass);
synchronized(_lock) {
_cacheDir = makeCacheDirs(host, port, user, pass, folder);
// Debugging only for now.
if (folder.equals(DIR_FOLDER))
File attach = null;
if (folder.equals(WebMail.DIR_FOLDER)) {
importMail();
} else if (folder.equals(WebMail.DIR_DRAFTS)) {
attach = new SecureDirectory(_cacheDir, DIR_ATTACHMENTS);
attach.mkdirs();
}
_attachmentDir = attach;
}
}
@@ -143,7 +151,7 @@ class PersistentMailCache {
int tcnt = Math.max(1, Math.min(sz / 4, Math.min(SystemVersion.getCores(), 16)));
List<Thread> threads = new ArrayList<Thread>(tcnt);
for (int i = 0; i < tcnt; i++) {
Thread t = new I2PAppThread(new Loader(fq, rv), "Email loader " + i);
Thread t = new I2PAppThread(new Loader(fq, rv, _isDrafts), "Email loader " + i);
t.start();
threads.add(t);
}
@@ -166,15 +174,17 @@ class PersistentMailCache {
private static class Loader implements Runnable {
private final Queue<File> _in;
private final Queue<Mail> _out;
private final boolean _isD;
public Loader(Queue<File> in, Queue<Mail> out) {
public Loader(Queue<File> in, Queue<Mail> out, boolean isDrafts) {
_in = in; _out = out;
_isD = isDrafts;
}
public void run() {
File f;
while ((f = _in.poll()) != null) {
Mail mail = load(f);
Mail mail = load(f, _isD);
if (mail != null)
_out.offer(mail);
}
@@ -296,14 +306,33 @@ class PersistentMailCache {
return base;
}
private File getHeaderFile(String uidl) {
public File getHeaderFile(String uidl) {
return getFile(uidl, HDR_SUFFIX);
}
private File getFullFile(String uidl) {
public File getFullFile(String uidl) {
return getFile(uidl, FULL_SUFFIX);
}
/**
* For reading or writing a new full mail (NOT headers only).
* For writing, caller MUST call writeComplete() on rv.
* Does not necessarily exist.
*
* @since 0.9.35
*/
public GzipFileBuffer getFullBuffer(String uidl) {
return new GzipFileBuffer(getFile(uidl, FULL_SUFFIX));
}
/**
* @return non-null only for Drafts
* @since 0.9.35
*/
public File getAttachmentDir() {
return _attachmentDir;
}
private File getFile(String uidl, String suffix) {
byte[] raw = DataHelper.getASCII(uidl);
byte[] md5 = PasswordManager.md5Sum(raw);
@@ -351,7 +380,7 @@ class PersistentMailCache {
*
* @return null on failure
*/
private static Mail load(File f) {
private static Mail load(File f, boolean isDrafts) {
String name = f.getName();
String uidl;
boolean headerOnly;
@@ -369,7 +398,11 @@ class PersistentMailCache {
Buffer rb = read(f);
if (rb == null)
return null;
Mail mail = new Mail(uidl);
Mail mail;
if (isDrafts)
mail = new Draft(uidl);
else
mail = new Mail(uidl);
if (headerOnly)
mail.setHeader(rb);
else

File diff suppressed because it is too large Load Diff

View File

@@ -131,7 +131,7 @@ public abstract class Encoding {
/**
* This implementation just converts the string to a byte array
* and then calls encode(byte[]).
* and then calls decode(byte[]).
* Most classes will not need to override.
*
* @param str

View File

@@ -134,14 +134,14 @@ public class HeaderLine extends Encoding {
* TODO this will not work for quoting structured text
* such as recipient names on the "To" and "Cc" lines.
*
* @param str must start with "field-name: "
* @param str must start with "field-name: ", must have non-whitespace after that
*/
@Override
public String encode(String str) throws EncodingException {
str = str.trim();
int l = str.indexOf(": ");
if (l <= 0 || l >= 64)
throw new EncodingException("bad 'field-name: '" + str);
throw new EncodingException("bad field-name: " + str);
l += 2;
boolean quote = false;
if (str.length() > 76) {

View File

@@ -221,7 +221,7 @@ public class SMTPClient {
}
/**
* @param body without the attachments
* @param body headers and body, without the attachments
* @param attachments may be null
* @param boundary non-null if attachments is non-null
* @return success
@@ -248,6 +248,7 @@ public class SMTPClient {
socket.setSoTimeout(120*1000);
int result = sendCmd(null);
if (result != 220) {
error += _t("Error sending mail") + '\n';
if (result != 0)
error += _t("Server refused connection") + " (" + result + ")\n";
else
@@ -331,39 +332,7 @@ public class SMTPClient {
//socket.getOutputStream().write(DataHelper.getASCII("\r\n.\r\n"));
// Do it this way so we don't double the memory
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"));
out.write(body.toString());
// moved from WebMail so we don't bring the attachments into memory
// Also TODO use the 250 service extension responses to pick the best encoding
// and check the max total size
if (attachments != null && !attachments.isEmpty()) {
for(Attachment attachment : attachments) {
String encodeTo = attachment.getTransferEncoding();
Encoding encoding = EncodingFactory.getEncoding(encodeTo);
if (encoding == null)
throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
// ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
// ref: RFC 2231
// split Content-Disposition into 3 lines to maximize room
// TODO filename*0* for long names...
String name = attachment.getFileName();
String name2 = FilenameUtil.sanitizeFilename(name);
String name3 = FilenameUtil.encodeFilenameRFC5987(name);
out.write("\r\n--" + boundary +
"\r\nContent-type: " + attachment.getContentType() +
"\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
"\";\r\n\tfilename*=" + name3 +
"\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
"\r\n\r\n");
InputStream in = null;
try {
in = attachment.getData();
encoding.encode(in, out);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
out.write( "\r\n--" + boundary + "--\r\n" );
}
writeMail(out, body, attachments, boundary);
out.write("\r\n.\r\n");
out.flush();
socket.setSoTimeout(0);
@@ -391,6 +360,50 @@ public class SMTPClient {
return mailSent;
}
/**
* Caller must close out
*
* @param body headers and body, without the attachments
* @param attachments may be null
* @param boundary non-null if attachments is non-null
*/
public static void writeMail(Writer out, StringBuilder body,
List<Attachment> attachments, String boundary) throws IOException {
out.write(body.toString());
// moved from WebMail so we don't bring the attachments into memory
// Also TODO use the 250 service extension responses to pick the best encoding
// and check the max total size
if (attachments != null && !attachments.isEmpty()) {
for(Attachment attachment : attachments) {
String encodeTo = attachment.getTransferEncoding();
Encoding encoding = EncodingFactory.getEncoding(encodeTo);
if (encoding == null)
throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
// ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
// ref: RFC 2231
// split Content-Disposition into 3 lines to maximize room
// TODO filename*0* for long names...
String name = attachment.getFileName();
String name2 = FilenameUtil.sanitizeFilename(name);
String name3 = FilenameUtil.encodeFilenameRFC5987(name);
out.write("\r\n--" + boundary +
"\r\nContent-type: " + attachment.getContentType() +
"\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
"\";\r\n\tfilename*=" + name3 +
"\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
"\r\n\r\n");
InputStream in = null;
try {
in = attachment.getData();
encoding.encode(in, out);
} finally {
if (in != null) try { in.close(); } catch (IOException ioe) {}
}
}
out.write( "\r\n--" + boundary + "--\r\n" );
}
}
/**
* A command to send and a result code to expect
* @since 0.9.13

View File

@@ -1,4 +1,12 @@
2018-04-14 zzz
* Console: Add built-by to /logs (ticket #2204)
* CPUID: Fix TBM detection (ticket #2211)
* Debian updates (ticket #2027, PR #15)
* Jetty: Fix quote in header line tripping XSS filter (ticket #2215)
* SusiMail: Add folders, drafts, background sending (ticket #2087)
2018-04-11 zzz
* Debian updates for 0.9.34
* Jetty 9.2.24-v201801015
* Tomcat 8.5.30

View File

@@ -18,7 +18,7 @@ public class RouterVersion {
/** deprecated */
public final static String ID = "Monotone";
public final static String VERSION = CoreVersion.VERSION;
public final static long BUILD = 1;
public final static long BUILD = 2;
/** for example "-test" */
public final static String EXTRA = "";