001: // The contents of this file are subject to the Mozilla Public License Version
002: // 1.1
003: //(the "License"); you may not use this file except in compliance with the
004: //License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
005: //
006: //Software distributed under the License is distributed on an "AS IS" basis,
007: //WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
008: //for the specific language governing rights and
009: //limitations under the License.
010: //
011: //The Original Code is "The Columba Project"
012: //
013: //The Initial Developers of the Original Code are Frederik Dietz and Timo
014: // Stich.
015: //Portions created by Frederik Dietz and Timo Stich are Copyright (C) 2003.
016: //
017: //All Rights Reserved.
018:
019: package org.columba.mail.composer;
020:
021: import java.io.BufferedReader;
022: import java.io.File;
023: import java.io.FileReader;
024: import java.io.IOException;
025: import java.io.InputStream;
026: import java.nio.charset.Charset;
027: import java.util.ArrayList;
028: import java.util.Date;
029: import java.util.Iterator;
030: import java.util.List;
031: import java.util.logging.Logger;
032:
033: import org.columba.api.command.IWorkerStatusController;
034: import org.columba.core.logging.Logging;
035: import org.columba.core.versioninfo.VersionInfo;
036: import org.columba.core.xml.XmlElement;
037: import org.columba.mail.config.AccountItem;
038: import org.columba.mail.config.Identity;
039: import org.columba.mail.config.MailConfig;
040: import org.columba.mail.config.SecurityItem;
041: import org.columba.mail.gui.composer.ComposerModel;
042: import org.columba.mail.message.PGPMimePart;
043: import org.columba.mail.message.SendableHeader;
044: import org.columba.mail.parser.ListBuilder;
045: import org.columba.mail.parser.ListParser;
046: import org.columba.mail.parser.text.HtmlParser;
047: import org.columba.ristretto.coder.EncodedWord;
048: import org.columba.ristretto.composer.MimeTreeRenderer;
049: import org.columba.ristretto.io.CharSequenceSource;
050: import org.columba.ristretto.message.Address;
051: import org.columba.ristretto.message.LocalMimePart;
052: import org.columba.ristretto.message.MessageDate;
053: import org.columba.ristretto.message.MessageIDGenerator;
054: import org.columba.ristretto.message.MimeHeader;
055: import org.columba.ristretto.message.MimePart;
056: import org.columba.ristretto.message.StreamableMimePart;
057: import org.columba.ristretto.parser.ParserException;
058:
059: public class MessageComposer {
060: /** JDK 1.4+ logging framework logger, used for logging. */
061: private static final Logger LOG = Logger
062: .getLogger("org.columba.mail.composer");
063:
064: private static final Charset headerCharset = Charset
065: .forName("UTF-8");
066:
067: private ComposerModel model;
068:
069: private int accountUid;
070:
071: public MessageComposer(ComposerModel model) {
072: this .model = model;
073: }
074:
075: protected SendableHeader initHeader() {
076: SendableHeader header = new SendableHeader();
077:
078: // RFC822 - Header
079: if (model.getToList() != null) {
080: String s = ListParser.createStringFromList(ListBuilder
081: .createFlatList(model.getToList()), ",");
082: if (s.length() != 0)
083: header.set("To", EncodedWord.encode(s, headerCharset,
084: EncodedWord.QUOTED_PRINTABLE).toString());
085: }
086:
087: if (model.getCcList() != null) {
088: String s = ListParser.createStringFromList(ListBuilder
089: .createFlatList(model.getCcList()), ",");
090: if (s.length() != 0)
091: header.set("Cc", EncodedWord.encode(s, headerCharset,
092: EncodedWord.QUOTED_PRINTABLE).toString());
093: }
094:
095: header.getAttributes().put("columba.subject",
096: model.getSubject());
097:
098: //header.set("Subject",
099: // EncodedWord.encode(model.getSubject(),
100: // Charset.forName(model.getCharsetName()),
101: // EncodedWord.QUOTED_PRINTABLE).toString());
102: header
103: .set("Subject", EncodedWord.encode(model.getSubject(),
104: headerCharset, EncodedWord.QUOTED_PRINTABLE)
105: .toString());
106:
107: AccountItem item = model.getAccountItem();
108: Identity identity = item.getIdentity();
109:
110: //mod: 20040629 SWITT for redirecting feature
111: //If FROM value was set, take this as From, else take Identity
112: if (model.getMessage().getHeader().getHeader().get("From") != null) {
113: header.set("From", EncodedWord.encode(
114: model.getMessage().getHeader().getHeader().get(
115: "From"), headerCharset,
116: EncodedWord.QUOTED_PRINTABLE).toString());
117: } else {
118: header.set("From", EncodedWord.encode(
119: identity.getAddress().toString(), headerCharset,
120: EncodedWord.QUOTED_PRINTABLE).toString());
121: }
122:
123: header.set("X-Priority", model.getPriority());
124:
125: /*
126: * String priority = controller.getModel().getPriority();
127: *
128: * if (priority != null) { header.set("columba.priority", new
129: * Integer(priority)); } else { header.set("columba.priority", new
130: * Integer(3)); }
131: */
132: header.set("Mime-Version", "1.0");
133:
134: String organisation = identity.getOrganisation();
135:
136: if (organisation != null && organisation.length() > 0) {
137: header.set("Organisation", organisation);
138: }
139:
140: // reply-to
141: Address replyAddress = identity.getReplyToAddress();
142:
143: if (replyAddress != null) {
144: header.set("Reply-To", EncodedWord.encode(
145: replyAddress.getMailAddress(), headerCharset,
146: EncodedWord.QUOTED_PRINTABLE).toString());
147: }
148:
149: String messageID = MessageIDGenerator.generate();
150: header.set("Message-ID", messageID);
151:
152: String inreply = model.getHeaderField("In-Reply-To");
153:
154: if (inreply != null) {
155: header.set("In-Reply-To", EncodedWord.encode(inreply,
156: headerCharset, EncodedWord.QUOTED_PRINTABLE)
157: .toString());
158: }
159:
160: String references = model.getHeaderField("References");
161:
162: if (references != null) {
163: header.set("References", references);
164: }
165:
166: header.set("X-Mailer", "Columba (" + VersionInfo.getVersion()
167: + ")");
168:
169: header.getAttributes().put("columba.from",
170: identity.getAddress());
171:
172: // date
173: Date date = new Date();
174: header.getAttributes().put("columba.date", date);
175: header.set("Date", MessageDate.toString(date));
176:
177: //attachments
178: header.getAttributes().put("columba.attachment",
179: new Boolean(model.getAttachments().size() > 0));
180:
181: // copy flags
182: header.setFlags(model.getMessage().getHeader().getFlags());
183:
184: return header;
185: }
186:
187: private boolean needQPEncoding(String input) {
188: for (int i = 0; i < input.length(); i++) {
189: if (input.charAt(i) > 127) {
190: return true;
191: }
192: }
193:
194: return false;
195: }
196:
197: /**
198: * gives the signature for this Mail back. This signature is NOT a
199: * pgp-signature but a real mail-signature.
200: *
201: * @param item
202: * The item wich holds the signature-file
203: * @return The signature for the mail as a String. The Signature is
204: * character encoded with the caracter set from the model
205: */
206: protected String getSignature(File file) {
207: StringBuffer strbuf = new StringBuffer();
208:
209: try {
210: BufferedReader in = new BufferedReader(new FileReader(file));
211:
212: /*
213: * BufferedReader in = new BufferedReader( new InputStreamReader(
214: * new FileInputStream(file), model.getCharsetName()));
215: */
216: String str;
217:
218: while ((str = in.readLine()) != null) {
219: strbuf.append(str + "\n");
220: }
221:
222: in.close();
223:
224: return strbuf.toString();
225: } catch (IOException ex) {
226: ex.printStackTrace();
227:
228: return "";
229: }
230: }
231:
232: /**
233: * Composes a multipart/alternative mime part for the body of a message
234: * containing a text part and a html part. <br>
235: * This is to be used for sending html messages, when an alternative text
236: * part - to be read by users not able to read html - is required. <br>
237: * Pre-condition: It is assumed that the model contains a message in html
238: * format.
239: *
240: * @return The composed mime part for the message body
241: * @author Karl Peder Olesen (karlpeder)
242: */
243: private StreamableMimePart composeMultipartAlternativeMimePart(
244: boolean appendSignature) {
245: // compose text part
246: StreamableMimePart textPart = composeTextMimePart(appendSignature);
247:
248: // compose html part
249: StreamableMimePart htmlPart = composeHtmlMimePart(appendSignature);
250:
251: // merge mimeparts and return
252: LocalMimePart bodyPart = new LocalMimePart(new MimeHeader(
253: "multipart", "alternative"));
254: bodyPart.addChild(textPart);
255: bodyPart.addChild(htmlPart);
256:
257: return bodyPart;
258: }
259:
260: /**
261: * Composes a text/html mime part from the body contained in the composer
262: * model. This could be for a pure html message or for the html part of a
263: * multipart/alternative. <br>
264: * If a signature is defined, it is added to the body. <br>
265: * Pre-condition: It is assumed that the model contains a html message.
266: *
267: * @return The composed text/html mime part
268: * @author Karl Peder Olesen (karlpeder)
269: */
270: private StreamableMimePart composeHtmlMimePart(
271: boolean appendSignature) {
272: // Init Mime-Header with Default-Values (text/html)
273: LocalMimePart bodyPart = new LocalMimePart(new MimeHeader(
274: "text", "html"));
275:
276: // Set Default Charset or selected
277: String charsetName = model.getCharset().name();
278:
279: StringBuffer buf = new StringBuffer();
280: String body = model.getBodyText();
281:
282: // insert link tags for urls and email addresses
283: body = HtmlParser.substituteURL(body, false);
284: body = HtmlParser.substituteEmailAddress(body, false);
285:
286: String lcase = body.toLowerCase(); // for text comparisons
287:
288: // insert document type decl.
289: if (lcase.indexOf("<!doctype") == -1) {
290: // FIXME (@author karlpeder): Is 3.2 the proper version of html to refer to?
291: buf.append("<!DOCTYPE HTML PUBLIC "
292: + "\"-//W3C//DTD HTML 3.2//EN\">\r\n");
293: }
294:
295: // insert head section with charset def.
296: String meta = "<meta " + "http-equiv=\"Content-Type\" "
297: + "content=\"text/html; charset=" + charsetName + "\">";
298: int pos = lcase.indexOf("<head");
299: int bodyStart;
300:
301: if (pos == -1) {
302: // add <head> section
303: pos = lcase.indexOf("<html") + 6;
304: buf.append(body.substring(0, pos));
305: buf.append("<head>");
306: buf.append(meta);
307: buf.append("</head>");
308:
309: bodyStart = pos;
310: } else {
311: // replace <head> section
312: pos = lcase.indexOf('>', pos) + 1;
313: buf.append(body.substring(0, pos));
314: buf.append(meta);
315:
316: // TODO (@author karlpeder): If existing meta tags are to be kept, code changes are
317: // necessary
318: bodyStart = lcase.indexOf("</head");
319: }
320:
321: // add rest of body until start of </body>
322: int bodyEnd = lcase.indexOf("</body");
323: buf.append(body.substring(bodyStart, bodyEnd));
324:
325: // add signature if defined
326: AccountItem item = model.getAccountItem();
327: Identity identity = item.getIdentity();
328: File signatureFile = identity.getSignature();
329:
330: if (signatureFile != null) {
331: String signature = getSignature(signatureFile);
332:
333: if (signature != null) {
334: buf.append("\r\n\r\n");
335:
336: // TODO: Should we take some action to ensure signature is valid
337: // html?
338: buf.append(signature);
339: }
340: }
341:
342: // add the rest of the original body - and transfer back to body var.
343: buf.append(body.substring(bodyEnd));
344: body = buf.toString();
345:
346: // add encoding if necessary
347: if (needQPEncoding(body)) {
348: bodyPart.getHeader().setContentTransferEncoding(
349: "quoted-printable");
350:
351: // check if the charset is US-ASCII then there is something wrong
352: // -> switch to UTF-8 and write to log-file
353: if (charsetName.equalsIgnoreCase("us-ascii")) {
354: charsetName = "UTF-8";
355: LOG
356: .info("Charset was US-ASCII but text has 8-bit chars -> switched to UTF-8");
357: }
358: }
359:
360: bodyPart.getHeader()
361: .putContentParameter("charset", charsetName);
362:
363: // to allow empty messages
364: if (body.length() == 0) {
365: body = " ";
366: }
367:
368: bodyPart.setBody(new CharSequenceSource(body));
369:
370: return bodyPart;
371: }
372:
373: /**
374: * Composes a text/plain mime part from the body contained in the composer
375: * model. This could be for a pure text message or for the text part of a
376: * multipart/alternative. <br>
377: * If the model contains a html message, tags are stripped to get plain
378: * text. <br>
379: * If a signature is defined, it is added to the body.
380: * @param appendSignature
381: *
382: * @return The composed text/plain mime part
383: */
384: private StreamableMimePart composeTextMimePart(
385: boolean appendSignature) {
386: // Init Mime-Header with Default-Values (text/plain)
387: LocalMimePart bodyPart = new LocalMimePart(new MimeHeader(
388: "text", "plain"));
389:
390: // Set Default Charset or selected
391: String charsetName = model.getCharset().name();
392:
393: String body = model.getBodyText();
394:
395: /*
396: * *20030918, karlpeder* Tags are stripped if the model contains a html
397: * message (since we are composing a plain text message here.
398: */
399: if (model.isHtml()) {
400: body = HtmlParser.htmlToText(body);
401: }
402:
403: AccountItem item = model.getAccountItem();
404: Identity identity = item.getIdentity();
405: File signatureFile = identity.getSignature();
406:
407: if (appendSignature && signatureFile != null) {
408: String signature = getSignature(signatureFile);
409:
410: if (signature != null) {
411: body = body + "\r\n\r\n" + signature;
412: }
413: }
414:
415: if (needQPEncoding(body)) {
416: bodyPart.getHeader().setContentTransferEncoding(
417: "quoted-printable");
418:
419: // check if the charset is US-ASCII then there is something wrong
420: // -> switch to UTF-8 and write to log-file
421: if (charsetName.equalsIgnoreCase("us-ascii")) {
422: charsetName = "UTF-8";
423: LOG
424: .info("Charset was US-ASCII but text has 8-bit chars -> switched to UTF-8");
425: }
426: }
427:
428: // write charset to header
429: bodyPart.getHeader()
430: .putContentParameter("charset", charsetName);
431:
432: // to allow empty messages
433: if (body.length() == 0) {
434: body = " ";
435: }
436:
437: bodyPart.setBody(new CharSequenceSource(body));
438:
439: return bodyPart;
440: }
441:
442: public SendableMessage compose(
443: IWorkerStatusController workerStatusController,
444: boolean appendSignature) throws Exception {
445: this .accountUid = model.getAccountItem().getUid();
446:
447: workerStatusController.setDisplayText("Composing Message...");
448:
449: MimeTreeRenderer renderer = MimeTreeRenderer.getInstance();
450: SendableMessage message = new SendableMessage();
451: SendableHeader header = initHeader();
452: MimePart root = null;
453:
454: /*
455: * *20030921, karlpeder* The old code was (accidentially!?) modifying
456: * the attachment list of the model. This affects the composing when
457: * called a second time for saving the message after sending!
458: */
459:
460: //List mimeParts = model.getAttachments();
461: List attachments = model.getAttachments();
462: List mimeParts = new ArrayList();
463: Iterator ite = attachments.iterator();
464:
465: while (ite.hasNext()) {
466: mimeParts.add(ite.next());
467: }
468:
469: // *20030919, karlpeder* Added handling of html messages
470: StreamableMimePart body;
471:
472: if (model.isHtml()) {
473: // compose message body as multipart/alternative
474: XmlElement composerOptions = MailConfig.getInstance()
475: .getComposerOptionsConfig().getRoot().getElement(
476: "/options");
477: XmlElement html = composerOptions.getElement("html");
478:
479: if (html == null) {
480: html = composerOptions.addSubElement("html");
481: }
482:
483: String multipart = html.getAttribute("send_as_multipart",
484: "true");
485:
486: if (multipart.equals("true")) {
487: // send as multipart/alternative
488: body = composeMultipartAlternativeMimePart(appendSignature);
489: } else {
490: // send as text/html
491: body = composeHtmlMimePart(appendSignature);
492: }
493: } else {
494: // compose message body as text/plain
495: body = composeTextMimePart(appendSignature);
496: }
497:
498: if (body != null) {
499: mimeParts.add(0, body);
500: }
501:
502: // Create Multipart/Mixed if necessary
503: if (mimeParts.size() > 1) {
504: root = new MimePart(new MimeHeader("multipart", "mixed"));
505:
506: for (int i = 0; i < mimeParts.size(); i++) {
507: root.addChild((StreamableMimePart) mimeParts.get(i));
508: }
509: } else {
510: root = (MimePart) mimeParts.get(0);
511: }
512:
513: if (model.isSignMessage()) {
514: SecurityItem item = model.getAccountItem().getPGPItem();
515: String idStr = item.get("id");
516:
517: // if the id not currently set (for example in the security panel in
518: // the account-config
519: if ((idStr == null) || (idStr.length() == 0)) {
520: // Set id on from address
521: item.setString("id", model.getAccountItem()
522: .getIdentity().getAddress().getMailAddress());
523: }
524:
525: PGPMimePart signPart = new PGPMimePart(new MimeHeader(
526: "multipart", "signed"), item);
527:
528: signPart.addChild(root);
529: root = signPart;
530: }
531:
532: if (model.isEncryptMessage()) {
533: SecurityItem item = model.getAccountItem().getPGPItem();
534:
535: // Set recipients from the recipients vector
536: List recipientList = model.getRCPTVector();
537: StringBuffer recipientBuf = new StringBuffer();
538:
539: for (Iterator it = recipientList.iterator(); it.hasNext();) {
540: recipientBuf.append((String) it.next());
541: }
542:
543: item.setString("recipients", recipientBuf.toString());
544:
545: PGPMimePart signPart = new PGPMimePart(new MimeHeader(
546: "multipart", "encrypted"), item);
547:
548: signPart.addChild(root);
549: root = signPart;
550: }
551:
552: header.setRecipients(model.getRCPTVector());
553:
554: List headerItemList;
555:
556: headerItemList = model.getToList();
557:
558: if ((headerItemList != null) && (headerItemList.size() > 0)) {
559: Address adr = null;
560: try {
561: adr = Address.parse((String) headerItemList.get(0));
562: header.getAttributes().put("columba.to", adr);
563: } catch (ParserException e) {
564: if (Logging.DEBUG)
565: e.printStackTrace();
566: }
567: }
568:
569: headerItemList = model.getCcList();
570:
571: if ((headerItemList != null) && (headerItemList.size() > 0)) {
572: Address adr = null;
573: try {
574: adr = Address.parse((String) headerItemList.get(0));
575: header.getAttributes().put("columba.cc", adr);
576: } catch (ParserException e) {
577: if (Logging.DEBUG)
578: e.printStackTrace();
579: }
580:
581: }
582:
583: root.getHeader().getHeader().merge(header.getHeader());
584:
585: InputStream in = renderer.renderMimePart(root);
586:
587: // size
588: int size = in.available() / 1024;
589: header.getAttributes().put("columba.size", new Integer(size));
590:
591: message.setHeader(header);
592:
593: message.setAccountUid(accountUid);
594:
595: //Do not access the inputstream after this line!
596: message.setSourceStream(in);
597:
598: return message;
599: }
600: }
|