001: //The contents of this file are subject to the Mozilla Public License Version 1.1
002: //(the "License"); you may not use this file except in compliance with the
003: //License. You may obtain a copy of the License at http://www.mozilla.org/MPL/
004: //
005: //Software distributed under the License is distributed on an "AS IS" basis,
006: //WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
007: //for the specific language governing rights and
008: //limitations under the License.
009: //
010: //The Original Code is "The Columba Project"
011: //
012: //The Initial Developers of the Original Code are Frederik Dietz and Timo Stich.
013: //Portions created by Frederik Dietz and Timo Stich are Copyright (C) 2003.
014: //
015: //All Rights Reserved.
016: package org.columba.mail.folder.command;
017:
018: import java.io.ByteArrayInputStream;
019: import java.io.File;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.nio.charset.Charset;
023: import java.text.DateFormat;
024: import java.util.ArrayList;
025: import java.util.Enumeration;
026: import java.util.List;
027: import java.util.logging.Logger;
028:
029: import javax.swing.JCheckBox;
030: import javax.swing.JFileChooser;
031: import javax.swing.JOptionPane;
032: import javax.swing.filechooser.FileFilter;
033:
034: import org.columba.api.command.ICommandReference;
035: import org.columba.api.command.IWorkerStatusController;
036: import org.columba.core.command.Command;
037: import org.columba.core.command.StatusObservableImpl;
038: import org.columba.core.command.Worker;
039: import org.columba.core.config.Config;
040: import org.columba.core.gui.frame.FrameManager;
041: import org.columba.core.io.DiskIO;
042: import org.columba.core.io.StreamUtils;
043: import org.columba.core.xml.XmlElement;
044: import org.columba.mail.command.IMailFolderCommandReference;
045: import org.columba.mail.config.MailConfig;
046: import org.columba.mail.folder.IMailbox;
047: import org.columba.mail.gui.message.util.DocumentParser;
048: import org.columba.mail.gui.message.viewer.AttachmentModel;
049: import org.columba.mail.parser.text.HtmlParser;
050: import org.columba.mail.util.MailResourceLoader;
051: import org.columba.ristretto.coder.Base64DecoderInputStream;
052: import org.columba.ristretto.coder.CharsetDecoderInputStream;
053: import org.columba.ristretto.coder.EncodedWord;
054: import org.columba.ristretto.coder.QuotedPrintableDecoderInputStream;
055: import org.columba.ristretto.message.BasicHeader;
056: import org.columba.ristretto.message.Header;
057: import org.columba.ristretto.message.MimeHeader;
058: import org.columba.ristretto.message.MimePart;
059: import org.columba.ristretto.message.MimeTree;
060: import org.columba.ristretto.message.StreamableMimePart;
061:
062: /**
063: * This class is used to save a message to file either as a html file or a text
064: * file.
065: *
066: * @author Karl Peder Olesen (karlpeder), 20030611
067: */
068: public class SaveMessageBodyAsCommand extends Command {
069:
070: /** JDK 1.4+ logging framework logger, used for logging. */
071: private static final Logger LOG = Logger
072: .getLogger("org.columba.mail.folder.command");
073:
074: /** Static field representing the system line separator */
075: private static final String NL = "\n";
076:
077: //System.getProperty("line.separator");
078:
079: /** The charset to use for decoding messages before save */
080: private Charset charset;
081:
082: private Header header;
083:
084: private MimeHeader bodyHeader;
085: private InputStream bodyStream;
086:
087: private List attachments;
088:
089: /**
090: * Constructor for SaveMessageBodyAsCommand. Calls super constructor and
091: * saves charset for later use
092: *
093: * @param references
094: * @param charset
095: * Charset to use for decoding messages before save
096: */
097: public SaveMessageBodyAsCommand(ICommandReference reference,
098: Charset charset) {
099: super (reference);
100: this .charset = charset;
101: }
102:
103: /**
104: * This method executes the save action, i.e. it saves the selected
105: * messages to disk as either plain text or as html. <br>At the momemt no
106: * header or attachment information is saved with the message!
107: *
108: * @param worker
109: * @see org.columba.api.command.Command#execute(Worker)
110: */
111: public void execute(IWorkerStatusController worker)
112: throws Exception {
113: IMailFolderCommandReference r = (IMailFolderCommandReference) getReference();
114: Object[] uids = r.getUids(); // uid for messages to save
115: IMailbox srcFolder = (IMailbox) r.getSourceFolder();
116:
117: // register for status events
118: ((StatusObservableImpl) srcFolder.getObservable())
119: .setWorker(worker);
120:
121: JFileChooser fileChooser = new JFileChooser();
122:
123: // save each message
124: for (int j = 0; j < uids.length; j++) {
125: Object uid = uids[j];
126: LOG.info("Saving UID=" + uid);
127:
128: header = srcFolder.getAllHeaderFields(uid);
129: setupMessageBodyPart(uid, srcFolder, worker);
130:
131: AttachmentModel attMod = new AttachmentModel();
132: attMod.setCollection(srcFolder.getMimePartTree(uid));
133:
134: attachments = attMod.getDisplayedMimeParts();
135:
136: // determine type of body part
137: boolean ishtml = false;
138:
139: if (bodyHeader.getMimeType().getSubtype().equals("html")) {
140: ishtml = true;
141: }
142:
143: // setup filters and filename for file chooser dialog
144: ExtensionFileFilter txtFilter = new ExtensionFileFilter(
145: "txt", "Text (*.txt)");
146: ExtensionFileFilter htmlFilter = new ExtensionFileFilter(
147: "html", "Html (*.html)");
148: fileChooser.resetChoosableFileFilters();
149: fileChooser.setAcceptAllFileFilterUsed(false);
150: fileChooser.addChoosableFileFilter(txtFilter);
151: fileChooser.addChoosableFileFilter(htmlFilter);
152:
153: // add check box for incl. of headers
154: JCheckBox inclHeaders = new JCheckBox(MailResourceLoader
155: .getString("dialog", "saveas", "save_all_headers"),
156: getInclAllHeadersOption());
157: fileChooser.setAccessory(inclHeaders);
158:
159: // setup dialog title, active filter and file name
160: String subject = EncodedWord.decode(header.get("Subject"))
161: .toString();
162: String defaultName = getValidFilename(subject, true);
163:
164: if (ishtml) {
165: fileChooser.setDialogTitle(MailResourceLoader
166: .getString("dialog", "saveas",
167: "save_html_message"));
168: fileChooser.setFileFilter(htmlFilter);
169:
170: if (defaultName.length() > 0) {
171: fileChooser.setSelectedFile(new File(defaultName
172: + "." + htmlFilter.getExtension()));
173: }
174: } else {
175: fileChooser.setDialogTitle(MailResourceLoader
176: .getString("dialog", "saveas",
177: "save_text_message"));
178: fileChooser.setFileFilter(txtFilter);
179:
180: if (defaultName.length() > 0) {
181: fileChooser.setSelectedFile(new File(defaultName
182: + "." + txtFilter.getExtension()));
183: }
184: }
185:
186: // show dialog
187: int res = fileChooser.showSaveDialog(null);
188:
189: if (res == JFileChooser.APPROVE_OPTION) {
190: File f = fileChooser.getSelectedFile();
191: ExtensionFileFilter filter = (ExtensionFileFilter) fileChooser
192: .getFileFilter();
193:
194: // Add default extension if no extension is given by the user
195: if (ExtensionFileFilter.getFileExtension(f) == null) {
196: f = new File(f.getAbsolutePath() + "."
197: + filter.getExtension());
198: }
199:
200: int confirm;
201:
202: if (f.exists()) {
203: // file exists, user needs to confirm overwrite
204: confirm = JOptionPane
205: .showConfirmDialog(FrameManager
206: .getInstance().getActiveFrame(),
207: MailResourceLoader.getString(
208: "dialog", "saveas",
209: "overwrite_existing_file"),
210: MailResourceLoader.getString(
211: "dialog", "saveas",
212: "file_exists"),
213: JOptionPane.YES_NO_OPTION,
214: JOptionPane.QUESTION_MESSAGE);
215: } else {
216: confirm = JOptionPane.YES_OPTION;
217: }
218:
219: if (confirm == JOptionPane.YES_OPTION) {
220: // store whether all headers should be incl.
221: boolean incl = inclHeaders.isSelected();
222: storeInclAllHeadersOption(incl);
223: LOG.info("Incl. all headers: " + incl);
224:
225: // save message
226: if (filter.getExtension().equals(
227: htmlFilter.getExtension())) {
228: saveMsgBodyAsHtml(incl, f);
229: } else {
230: saveMsgBodyAsText(incl, f);
231: }
232: }
233: }
234: }
235:
236: // end of for loop over uids to save
237: }
238:
239: /**
240: * Private utility to extract a valid filename from a message subject or
241: * another string. <br>This means remove the chars: / \ : , \n \t NB: If
242: * the input string is null, an empty string is returned
243: *
244: * @param subj
245: * Message subject
246: * @param replSpaces
247: * If true, spaces are replaced by _
248: * @return A valid filename without the chars mentioned
249: */
250: private String getValidFilename(String subj, boolean replSpaces) {
251: if (subj == null) {
252: return "";
253: }
254:
255: StringBuffer buf = new StringBuffer();
256:
257: for (int i = 0; i < subj.length(); i++) {
258: char c = subj.charAt(i);
259:
260: if ((c == '\\') || (c == '/') || (c == ':') || (c == ',')
261: || (c == '\n') || (c == '\t') || (c == '(')
262: || (c == ')') || (c == '[') || (c == ']')) {
263: // dismiss char
264: } else if ((c == ' ') && (replSpaces)) {
265: buf.append('_');
266: } else {
267: buf.append(c);
268: }
269: }
270:
271: return buf.toString();
272: }
273:
274: /**
275: * Gets the value of the option "Incl. all headers"
276: *
277: * @return true if all headers should be included, else false
278: */
279: private boolean getInclAllHeadersOption() {
280: boolean defaultValue = false; // default value
281:
282: XmlElement options = Config.getInstance().get("options")
283: .getElement("/options");
284:
285: if (options == null) {
286: return defaultValue;
287: }
288:
289: XmlElement savemsg = options.getElement("/savemsg");
290:
291: if (savemsg != null) {
292: if (savemsg.getAttribute("incl_all_headers",
293: String.valueOf(defaultValue)).equals("true")) {
294: return true;
295: } else {
296: return false;
297: }
298: } else {
299: return defaultValue;
300: }
301: }
302:
303: /**
304: * Saves the option "Incl. all headers"
305: *
306: * @param val
307: * Value of the option (true to incl. all headers)
308: */
309: private void storeInclAllHeadersOption(boolean val) {
310: XmlElement options = Config.getInstance().get("options")
311: .getElement("/options");
312:
313: if (options == null) {
314: return;
315: }
316:
317: XmlElement savemsg = options.getElement("/savemsg");
318:
319: if (savemsg == null) {
320: // create new
321: savemsg = new XmlElement("savemsg");
322: savemsg.addAttribute("incl_all_headers", String
323: .valueOf(val));
324: options.addElement(savemsg);
325: } else {
326: savemsg.addAttribute("incl_all_headers", String
327: .valueOf(val));
328: }
329: }
330:
331: /**
332: * Private utility to get body part of a message. User preferences
333: * regarding html messages is used to select what to retrieve. If the body
334: * part retrieved is null, a fake one containing a simple text is returned
335: *
336: * @param uid
337: * ID of message
338: * @param srcFolder
339: * AbstractMessageFolder containing the message
340: * @param worker
341: * @return body part of message
342: */
343: private void setupMessageBodyPart(Object uid, IMailbox srcFolder,
344: IWorkerStatusController worker) throws Exception {
345: // Does the user prefer html or plain text?
346: XmlElement html = MailConfig.getInstance()
347: .getMainFrameOptionsConfig().getRoot().getElement(
348: "/options/html");
349:
350: // Get body of message depending on user preferences
351: MimeTree mimePartTree = srcFolder.getMimePartTree(uid);
352:
353: MimePart bodyPart = null;
354:
355: if (Boolean.valueOf(html.getAttribute("prefer")).booleanValue()) {
356: bodyPart = mimePartTree.getFirstTextPart("html");
357: } else {
358: bodyPart = mimePartTree.getFirstTextPart("plain");
359: }
360:
361: if (bodyPart == null) {
362: bodyHeader = new MimeHeader();
363: bodyStream = new ByteArrayInputStream(new byte[0]);
364: } else {
365: bodyHeader = bodyPart.getHeader();
366: bodyStream = srcFolder.getMimePartBodyStream(uid, bodyPart
367: .getAddress());
368: }
369: }
370:
371: /**
372: * Private utility to decode the message body with the proper charset
373: *
374: * @param bodyPart
375: * The body of the message
376: * @return Decoded message body
377: * @author Karl Peder Olesen (karlpeder), 20030601
378: */
379: private String getDecodedMessageBody() throws IOException {
380: int encoding = bodyHeader.getContentTransferEncoding();
381:
382: switch (encoding) {
383: case MimeHeader.QUOTED_PRINTABLE: {
384: bodyStream = new QuotedPrintableDecoderInputStream(
385: bodyStream);
386:
387: break;
388: }
389:
390: case MimeHeader.BASE64: {
391: bodyStream = new Base64DecoderInputStream(bodyStream);
392:
393: break;
394: }
395: }
396:
397: // First determine which charset to use
398: if (charset == null) {
399: try {
400: // get charset from message
401: charset = Charset.forName(bodyHeader
402: .getContentParameter("charset"));
403: } catch (Exception ex) {
404: // decode using default charset
405: charset = Charset.forName(System
406: .getProperty("file.encoding"));
407: }
408: }
409:
410: bodyStream = new CharsetDecoderInputStream(bodyStream, charset);
411:
412: return StreamUtils.readCharacterStream(bodyStream).toString();
413: }
414:
415: /**
416: * Method for saving a message body as a html file. No headers are saved
417: * with the message.
418: *
419: * @param header
420: * Message headers
421: * @param bodyPart
422: * Body of message
423: * @param attachments
424: * List of attachments as MimePart objects
425: * @param inclAllHeaders
426: * If true all (except Content-Type and Mime-Version) headers
427: * are output. If false, only a small subset is included
428: * @param file
429: * File to output to
430: */
431: private void saveMsgBodyAsHtml(boolean inclAllHeaders, File file)
432: throws IOException {
433: // decode message body with respect to charset
434: String decodedBody = getDecodedMessageBody();
435:
436: String body;
437:
438: if (!bodyHeader.getMimeType().getSubtype().equals("html")) {
439: try {
440: // substitute special characters like: <,>,&,\t,\n
441: body = HtmlParser
442: .substituteSpecialCharacters(decodedBody);
443:
444: // parse for urls / email adr. and substite with HTML-code
445: body = HtmlParser.substituteURL(body);
446: body = HtmlParser.substituteEmailAddress(body);
447:
448: // mark quotings with special font
449: body = DocumentParser.markQuotings(body);
450: } catch (Exception e) {
451: LOG.severe("Error parsing body: " + e.getMessage());
452: body = "<em>Error parsing body!!!</em>";
453: }
454:
455: // encapsulate bodytext in html-code
456: String css = getDefaultStyleSheet();
457: body = "<html><head>" + NL + css + NL
458: + "<title>E-mail saved by Columba</title>" + NL
459: + "</head><body><p class=\"bodytext\">" + NL + body
460: + NL + "</p></body></html>";
461: } else {
462: // use body as is
463: body = HtmlParser.validateHTMLString(decodedBody);
464: }
465:
466: // headers
467: String[][] headers = getHeadersToSave(inclAllHeaders);
468: String msg = insertHtmlHeaderTable(body, headers);
469:
470: // save message
471: try {
472: DiskIO.saveStringInFile(file, msg);
473: LOG.fine("Html msg saved as " + file.getAbsolutePath());
474: } catch (IOException ioe) {
475: LOG.severe("Error saving message to file: "
476: + ioe.getMessage());
477: }
478: }
479:
480: /**
481: * Defines and returns a default stylesheet for use when text messages are
482: * saved as html. <br>This stylesheet should be the same as the one
483: * defined in TextViewer for use when displaying text messages.
484: */
485: private String getDefaultStyleSheet() {
486: // read configuration from options.xml file
487: XmlElement textFont = Config.getInstance().get("options")
488: .getElement("/options/gui/textfont");
489: String name = textFont.getAttribute("name");
490: String size = textFont.getAttribute("size");
491:
492: // create css-stylesheet string
493: String css = "<style type=\"text/css\"><!-- .bodytext {font-family:\""
494: + name
495: + "\"; font-size:\""
496: + size
497: + "pt; \"}"
498: + ".quoting {color:#949494;}; --></style>";
499:
500: return css;
501: }
502:
503: /**
504: * Inserts a table with headers in a html message. The table is inserted
505: * just after the body tag.
506: *
507: * @param body
508: * Original message body
509: * @param headers
510: * Array with headers (keys and values)
511: * @return message body with header table inserted
512: */
513: private String insertHtmlHeaderTable(String body, String[][] headers) {
514: // create header table
515: StringBuffer buf = new StringBuffer();
516: String csskey = "border: 1px solid black; font-size: 8pt; font-weight: bold;";
517: String cssval = "border: 1px solid black; font-size: 8pt;";
518: buf
519: .append("<table style=\"background-color: #dddddd;\" cellspacing=\"0\">");
520: buf.append(NL);
521:
522: for (int i = 0; i < headers[0].length; i++) {
523: // process header value
524: String val = headers[1][i];
525:
526: try {
527: val = HtmlParser
528: .substituteSpecialCharactersInHeaderfields(val);
529: val = HtmlParser.substituteURL(val);
530: val = HtmlParser.substituteEmailAddress(val);
531: } catch (Exception e) {
532: LOG.severe("Error parsing header value: "
533: + e.getMessage());
534: }
535:
536: buf.append("<tr><td style=\"" + csskey + "\">");
537: buf.append(headers[0][i]);
538: buf.append("</td>");
539: buf.append("<td style=\"" + cssval + "\">");
540: buf.append(val);
541: buf.append("</td></tr>");
542: buf.append(NL);
543: }
544:
545: buf.append("</table>");
546: buf.append("<br>" + NL);
547:
548: String headertbl = buf.toString();
549:
550: // insert into message right after <body...>
551: int pos = body.toLowerCase().indexOf("<body");
552: pos = body.indexOf(">", pos);
553: pos++;
554:
555: String msg = body.substring(0, pos) + headertbl
556: + body.substring(pos);
557:
558: return msg;
559: }
560:
561: /**
562: * Method for saving a message in a text file.
563: *
564: * @param header Message headers
565: * @param bodyPart Body of message
566: * @param attachments List of attachments as MimePart objects
567: * @param inclAllHeaders If true all (except Content-Type and Mime-Version) headers
568: * are output. If false, only a small subset is included
569: * @param file File to output to
570: */
571: private void saveMsgBodyAsText(boolean inclAllHeaders, File file)
572: throws IOException {
573: //DocumentParser parser = new DocumentParser();
574: // decode message body with respect to charset
575: String decodedBody = getDecodedMessageBody();
576:
577: String body;
578:
579: if (bodyHeader.getMimeType().getSubtype().equals("html")) {
580: // strip tags
581: //body = parser.stripHTMLTags(decodedBody, true);
582: //body = parser.restoreSpecialCharacters(body);
583: body = HtmlParser.htmlToText(decodedBody);
584: } else {
585: // use body as is
586: body = decodedBody;
587: }
588:
589: // headers
590: String[][] headers = getHeadersToSave(inclAllHeaders);
591: StringBuffer buf = new StringBuffer();
592:
593: for (int i = 0; i < headers[0].length; i++) {
594: buf.append(headers[0][i]);
595: buf.append(": ");
596: buf.append(headers[1][i]);
597: buf.append(NL);
598: }
599:
600: buf.append(NL);
601:
602: // message composed of headers and body
603: String msg = buf.toString() + body;
604:
605: // save message
606: DiskIO.saveStringInFile(file, msg);
607: LOG.fine("Text msg saved as " + file.getAbsolutePath());
608: }
609:
610: /**
611: * Private utility to get headers to save. Headers are returned in a 2D
612: * array, so [0][i] is key[i] and [1][i] is value[i].
613: *
614: * @param header All message headers
615: * @param attachments Attachments, header lines with file names are added
616: * @param inclAll true if all headers except Content-Type and Mime-Version
617: * should be included
618: * @return Array of headers to include when saving
619: */
620: private String[][] getHeadersToSave(boolean inclAll) {
621: List keyList = new ArrayList();
622: List valueList = new ArrayList();
623: BasicHeader basicHeader = new BasicHeader(header);
624:
625: String from = basicHeader.getFrom().toString();
626: String to = (basicHeader.getTo()[0]).toString();
627:
628: DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG,
629: DateFormat.MEDIUM);
630: String date = df.format(basicHeader.getDate());
631:
632: String subject = basicHeader.getSubject();
633:
634: // loop over all headers
635: Enumeration keys = header.getKeys();
636:
637: while (keys.hasMoreElements()) {
638: String key = (String) keys.nextElement();
639:
640: if (key.equals("From")) {
641: } else if (key.equals("To")) {
642: } else if (key.equals("Subject")) {
643: } else if (key.equals("Date")) {
644: // ignore - columba.date is used instead
645: } else if (key.startsWith("Content-")) {
646: // ignore
647: } else if (key.equals("Mime-Version")
648: || key.equals("MIME-Version")) {
649: // ignore
650: } else if (key.startsWith("columba")) {
651: // ignore
652: } else {
653: if (inclAll) {
654: // all headers should be included
655: keyList.add(key);
656: valueList.add((String) header.get(key));
657: }
658: }
659: }
660:
661: // add from, to, date, subj so they are the last elements
662: keyList.add(MailResourceLoader.getString("header", "from"));
663: valueList.add(from);
664: keyList.add(MailResourceLoader.getString("header", "to"));
665: valueList.add(to);
666: keyList.add(MailResourceLoader.getString("header", "date"));
667: valueList.add(date);
668: keyList.add(MailResourceLoader.getString("header", "subject"));
669: valueList.add(subject);
670:
671: for (int i = 0; i < attachments.size(); i++) {
672: String name = ((StreamableMimePart) attachments.get(i))
673: .getHeader().getFileName();
674:
675: if (name != null) {
676: keyList.add(MailResourceLoader.getString("header",
677: "attachment"));
678: valueList.add(name);
679: }
680: }
681:
682: // create array and return
683: String[][] headerArray = new String[2][];
684: headerArray[0] = new String[keyList.size()];
685: headerArray[1] = new String[keyList.size()];
686:
687: for (int i = 0; i < keyList.size(); i++) {
688: headerArray[0][i] = (String) keyList.get(i);
689: headerArray[1][i] = (String) valueList.get(i);
690: }
691:
692: return headerArray;
693: }
694: }
695:
696: /**
697: * Represents a file filter selecting only a given type of files. <br>
698: * Extension is used to recognize files. <br>Default file type is txt files.
699: */
700: class ExtensionFileFilter extends FileFilter {
701: /** extension to accept */
702: private String extension = "txt";
703:
704: /** description of the file type */
705: private String description = "Text files (*.txt)";
706:
707: /** Constructor setting the extension to accept and a type description */
708: public ExtensionFileFilter(String extension, String description) {
709: super ();
710: this .extension = extension;
711: this .description = description;
712: }
713:
714: /** Returns true if a given file is of the correct type */
715: public boolean accept(File f) {
716: if (f.isDirectory()) {
717: return true;
718: }
719:
720: // test on extension
721: String ext = getFileExtension(f);
722:
723: if ((ext != null) && (this .extension.toLowerCase().equals(ext))) {
724: return true;
725: } else {
726: return false;
727: }
728: }
729:
730: /**
731: * Static method for extracting the extension of a filename
732: *
733: * @return f File to get extension for
734: * @return extension or null if no extension exist
735: */
736: public static String getFileExtension(File f) {
737: String ext = null;
738: String s = f.getName();
739: int i = s.lastIndexOf('.');
740:
741: if ((i > 0) && (i < (s.length() - 1))) {
742: ext = s.substring(i + 1).toLowerCase();
743: }
744:
745: return ext;
746: }
747:
748: /** Returns the description of this filter / file type */
749: public String getDescription() {
750: return this .description;
751: }
752:
753: /** Returns the extension used by this filter */
754: public String getExtension() {
755: return this.extension;
756: }
757: }
|