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) 2006.
016: //
017: //All Rights Reserved.
018: package org.columba.mail.gui.message.viewer;
019:
020: import java.awt.BorderLayout;
021: import java.awt.Font;
022: import java.awt.event.MouseEvent;
023: import java.awt.event.MouseListener;
024: import java.io.ByteArrayInputStream;
025: import java.io.File;
026: import java.io.FileOutputStream;
027: import java.io.InputStream;
028: import java.net.MalformedURLException;
029: import java.net.URL;
030: import java.nio.charset.Charset;
031: import java.util.Iterator;
032: import java.util.List;
033: import java.util.Observable;
034: import java.util.Observer;
035: import java.util.logging.Logger;
036: import java.util.regex.Matcher;
037: import java.util.regex.Pattern;
038:
039: import javax.swing.BorderFactory;
040: import javax.swing.JComponent;
041: import javax.swing.JEditorPane;
042: import javax.swing.JOptionPane;
043: import javax.swing.JPanel;
044: import javax.swing.JPopupMenu;
045: import javax.swing.SwingUtilities;
046: import javax.swing.event.CaretEvent;
047: import javax.swing.event.CaretListener;
048: import javax.swing.text.AttributeSet;
049: import javax.swing.text.BadLocationException;
050: import javax.swing.text.Element;
051: import javax.swing.text.html.HTML;
052: import javax.swing.text.html.HTMLDocument;
053:
054: import org.columba.api.plugin.IExtension;
055: import org.columba.api.plugin.IExtensionHandler;
056: import org.columba.api.plugin.IExtensionHandlerKeys;
057: import org.columba.api.plugin.PluginException;
058: import org.columba.api.plugin.PluginHandlerNotFoundException;
059: import org.columba.core.charset.CharsetOwnerInterface;
060: import org.columba.core.config.Config;
061: import org.columba.core.desktop.ColumbaDesktop;
062: import org.columba.core.gui.frame.DefaultContainer;
063: import org.columba.core.gui.htmlviewer.api.IHTMLViewerPlugin;
064: import org.columba.core.gui.util.FontProperties;
065: import org.columba.core.io.StreamUtils;
066: import org.columba.core.logging.Logging;
067: import org.columba.core.plugin.PluginManager;
068: import org.columba.core.util.TempFileStore;
069: import org.columba.core.xml.XmlElement;
070: import org.columba.mail.config.MailConfig;
071: import org.columba.mail.config.OptionsItem;
072: import org.columba.mail.folder.IMailbox;
073: import org.columba.mail.gui.composer.ComposerController;
074: import org.columba.mail.gui.composer.ComposerModel;
075: import org.columba.mail.gui.frame.MailFrameMediator;
076: import org.columba.mail.gui.message.IMessageController;
077: import org.columba.mail.gui.message.action.AddToAddressbookAction;
078: import org.columba.mail.gui.message.action.ComposeMessageAction;
079: import org.columba.mail.gui.message.action.CopyLinkLocationAction;
080: import org.columba.mail.gui.message.action.OpenAction;
081: import org.columba.mail.gui.message.action.OpenWithAction;
082: import org.columba.mail.gui.message.util.ColumbaURL;
083: import org.columba.mail.parser.text.HtmlParser;
084: import org.columba.ristretto.coder.Base64DecoderInputStream;
085: import org.columba.ristretto.coder.FallbackCharsetDecoderInputStream;
086: import org.columba.ristretto.coder.QuotedPrintableDecoderInputStream;
087: import org.columba.ristretto.message.MimeHeader;
088: import org.columba.ristretto.message.MimePart;
089: import org.columba.ristretto.message.MimeTree;
090:
091: /**
092: * Display message body text.
093: *
094: * @author fdietz
095: */
096: public class TextViewer extends JPanel implements IMimePartViewer,
097: Observer, CaretListener {
098:
099: /** JDK 1.4+ logging framework logger, used for logging. */
100: private static final Logger LOG = Logger
101: .getLogger("org.columba.mail.gui.message.viewer");
102:
103: private static final Pattern CIDPattern = Pattern.compile(
104: "cid:([^\"]+)", Pattern.CASE_INSENSITIVE);
105:
106: // stylesheet is created dynamically because
107: // user configurable fonts are used
108: private String css = "";
109:
110: // enable/disable smilies configuration
111: private XmlElement smilies;
112:
113: private boolean enableSmilies;
114:
115: // name of font
116: private String name;
117:
118: /*
119: * private String body;
120: *
121: * private URL url;
122: */
123:
124: private String body;
125:
126: /**
127: * if true, a html message is shown. Otherwise, plain/text
128: */
129: private boolean htmlMessage;
130:
131: private IMessageController mediator;
132:
133: private IHTMLViewerPlugin viewerPlugin;
134:
135: private IMailbox folder;
136:
137: private Object uid;
138:
139: private boolean usingJDIC;
140:
141: public TextViewer(IMessageController mediator) {
142: super ();
143:
144: this .mediator = mediator;
145:
146: initHTMLViewerPlugin();
147:
148: setLayout(new BorderLayout());
149:
150: add(viewerPlugin.getContainer(), BorderLayout.CENTER);
151:
152: initConfiguration();
153:
154: initStyleSheet();
155:
156: if (!usingJDIC)
157: viewerPlugin.getComponent().addMouseListener(
158: new URLMouseListener());
159:
160: setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
161: }
162:
163: private void initHTMLViewerPlugin() {
164: OptionsItem optionsItem = MailConfig.getInstance()
165: .getOptionsItem();
166: String selectedBrowser = optionsItem.getStringWithDefault(
167: OptionsItem.MESSAGEVIEWER,
168: OptionsItem.SELECTED_BROWSER, "Default");
169:
170: try {
171: viewerPlugin = createHTMLViewerPluginInstance(selectedBrowser);
172: // in case of an error -> fall-back to Swing's built-in JTextPane
173: if (viewerPlugin == null || !viewerPlugin.initialized()) {
174: JOptionPane.showMessageDialog(this ,
175: "Error while trying to load html viewer");
176:
177: LOG
178: .severe("Error while trying to load html viewer -> falling back to default");
179:
180: viewerPlugin = createHTMLViewerPluginInstance("Default");
181: }
182: } catch (Exception e) {
183: viewerPlugin = createHTMLViewerPluginInstance("Default");
184:
185: if (Logging.DEBUG)
186: e.printStackTrace();
187: } catch (Error e) {
188: viewerPlugin = createHTMLViewerPluginInstance("Default");
189:
190: if (Logging.DEBUG)
191: e.printStackTrace();
192: }
193:
194: }
195:
196: private JPopupMenu createPopupMenu(ColumbaURL url) {
197: JPopupMenu menu = new JPopupMenu();
198: menu.add(new CopyLinkLocationAction(url));
199: menu.addSeparator();
200: menu.add(new OpenAction(url));
201: menu.add(new OpenWithAction(url));
202: menu.addSeparator();
203: menu.add(new AddToAddressbookAction(url));
204: menu.add(new ComposeMessageAction(url));
205:
206: return menu;
207: }
208:
209: private IHTMLViewerPlugin createHTMLViewerPluginInstance(
210: String pluginId) {
211: IHTMLViewerPlugin plugin = null;
212: try {
213:
214: IExtensionHandler handler = PluginManager
215: .getInstance()
216: .getExtensionHandler(
217: IExtensionHandlerKeys.ORG_COLUMBA_CORE_HTMLVIEWER);
218:
219: IExtension extension = handler.getExtension(pluginId);
220: if (extension == null)
221: return null;
222:
223: plugin = (IHTMLViewerPlugin) extension
224: .instanciateExtension(null);
225:
226: return plugin;
227: } catch (PluginHandlerNotFoundException e) {
228: LOG.severe("Error while loading viewer plugin: "
229: + e.getMessage());
230: if (Logging.DEBUG)
231: e.printStackTrace();
232: } catch (PluginException e) {
233: LOG.severe("Error while loading viewer plugin: "
234: + e.getMessage());
235: if (Logging.DEBUG)
236: e.printStackTrace();
237: }
238:
239: return null;
240: }
241:
242: /**
243: *
244: */
245: private void initConfiguration() {
246: XmlElement gui = MailConfig.getInstance().get("options")
247: .getElement("/options/gui");
248: XmlElement messageviewer = gui.getElement("messageviewer");
249:
250: if (messageviewer == null) {
251: messageviewer = gui.addSubElement("messageviewer");
252: }
253:
254: messageviewer.addObserver(this );
255:
256: smilies = messageviewer.getElement("smilies");
257:
258: if (smilies == null) {
259: smilies = messageviewer.addSubElement("smilies");
260: }
261:
262: // register as configuration change listener
263: smilies.addObserver(this );
264:
265: String enable = smilies.getAttribute("enabled", "true");
266:
267: if (enable.equals("true")) {
268: enableSmilies = true;
269: } else {
270: enableSmilies = false;
271: }
272:
273: XmlElement quote = messageviewer.getElement("quote");
274:
275: if (quote == null) {
276: quote = messageviewer.addSubElement("quote");
277: }
278:
279: // register as configuration change listener
280: quote.addObserver(this );
281:
282: // register for configuration changes
283: Font font = FontProperties.getTextFont();
284: name = font.getName();
285:
286: XmlElement options = Config.getInstance().get("options")
287: .getElement("/options");
288: XmlElement gui1 = options.getElement("gui");
289: XmlElement fonts = gui1.getElement("fonts");
290:
291: if (fonts == null) {
292: fonts = gui1.addSubElement("fonts");
293: }
294:
295: // register interest on configuratin changes
296: fonts.addObserver(this );
297:
298: // XmlElement selectedBrowser =
299: // messageviewer.getElement(OptionsItem.SELECTED_BROWSER);
300: // selectedBrowser.addObserver(this);
301:
302: }
303:
304: /**
305: * @see org.columba.mail.gui.message.viewer.IMimePartViewer#view(org.columba.mail.folder.IMailbox,
306: * java.lang.Object, java.lang.Integer[],
307: * org.columba.mail.gui.frame.MailFrameMediator)
308: */
309: public void view(IMailbox folder, Object uid, Integer[] address,
310: MailFrameMediator mediator) throws Exception {
311:
312: this .folder = folder;
313: this .uid = uid;
314:
315: MimePart bodyPart = null;
316: InputStream bodyStream;
317:
318: MimeTree mimePartTree = folder.getMimePartTree(uid);
319:
320: bodyPart = mimePartTree.getFromAddress(address);
321:
322: if (bodyPart == null) {
323: bodyStream = new ByteArrayInputStream("<No Message-Text>"
324: .getBytes());
325: } else {
326: // Shall we use the HTML-IViewer?
327: htmlMessage = bodyPart.getHeader().getMimeType()
328: .getSubtype().equals("html");
329:
330: bodyStream = folder.getMimePartBodyStream(uid, bodyPart
331: .getAddress());
332: }
333:
334: bodyStream = MessageParser.decodeBodyStream(bodyPart,
335: bodyStream);
336:
337: // Which Charset shall we use ?
338: if (!htmlMessage) {
339: Charset charset = ((CharsetOwnerInterface) mediator)
340: .getCharset();
341: charset = MessageParser.extractCharset(charset, bodyPart);
342:
343: bodyStream = new FallbackCharsetDecoderInputStream(
344: bodyStream, charset);
345: }
346:
347: // Read Stream in String
348: StringBuffer text = StreamUtils.readCharacterStream(bodyStream);
349:
350: // if HTML stripping is enabled
351: if (isHTMLStrippingEnabled()) {
352: // strip HTML message -> remove all HTML tags
353: text = new StringBuffer(HtmlParser.stripHtmlTags(text
354: .toString(), true));
355:
356: htmlMessage = false;
357: }
358:
359: if (htmlMessage) {
360: // this is a HTML message
361: body = text.toString();
362:
363: // Download any CIDs in the html mail
364: body = downloadCIDParts(body, mimePartTree);
365:
366: } else {
367: // this is a text/plain message
368:
369: body = MessageParser.transformTextToHTML(text.toString(),
370: css, enableSmilies);
371:
372: // setText(body);
373:
374: }
375:
376: }
377:
378: private boolean isHTMLStrippingEnabled() {
379: XmlElement html = MailConfig.getInstance()
380: .getMainFrameOptionsConfig().getRoot().getElement(
381: "/options/html");
382:
383: return Boolean.valueOf(html.getAttribute("disable"))
384: .booleanValue();
385: }
386:
387: /**
388: *
389: * read text-properties from configuration and create a stylesheet for the
390: * html-document
391: *
392: */
393: private void initStyleSheet() {
394: // read configuration from options.xml file
395: // create css-stylesheet string
396: // set font of html-element <P>
397:
398: /*
399: * css = "<style type=\"text/css\">\n" + "body {font-family:\"" + name +
400: * "\"; font-size:\"" + size + "pt; \"} \n" + "a { color: blue;
401: * text-decoration: underline }\n" + "font.quoting {color:#949494;} \n" + "</style>\n";
402: */
403:
404: css = "<style type=\"text/css\">\n" + "body {font-family:\""
405: + name + "\";} \n"
406: + "a { color: blue; text-decoration: underline }\n"
407: + "font.quoting {color:#949494;} \n" + "</style>\n";
408:
409: }
410:
411: /*
412: * (non-Javadoc)
413: *
414: * @see org.columba.mail.gui.config.general.MailOptionsDialog
415: *
416: * @see java.util.Observer#update(java.util.Observable, java.lang.Object)
417: */
418: public void update(Observable arg0, Object arg1) {
419: Font font = FontProperties.getTextFont();
420: name = font.getName();
421:
422: initStyleSheet();
423:
424: // remove old renderer
425: remove(viewerPlugin.getContainer());
426:
427: // init new renderer
428: initHTMLViewerPlugin();
429:
430: // add new renderer
431: add(viewerPlugin.getContainer(), BorderLayout.CENTER);
432: }
433:
434: public String getSelectedText() {
435: return viewerPlugin.getSelectedText();
436: }
437:
438: public String getText() {
439: return viewerPlugin.getText();
440: }
441:
442: public void setCaretPosition(int position) {
443: viewerPlugin.setCaretPosition(position);
444: }
445:
446: public void moveCaretPosition(int position) {
447: viewerPlugin.moveCaretPosition(position);
448: }
449:
450: /**
451: * @see org.columba.mail.gui.message.viewer.IViewer#getView()
452: */
453: public JComponent getView() {
454: return viewerPlugin.getContainer();
455: }
456:
457: /**
458: * @see org.columba.mail.gui.message.viewer.IViewer#updateGUI()
459: */
460: public void updateGUI() throws Exception {
461: viewerPlugin.view(body);
462: }
463:
464: /**
465: * @see javax.swing.event.CaretListener#caretUpdate(javax.swing.event.CaretEvent)
466: */
467: public void caretUpdate(CaretEvent arg0) {
468: // FocusManager.getInstance().updateActions();
469: }
470:
471: private String downloadCIDParts(String body, MimeTree mimeTree) {
472: Matcher matcher = CIDPattern.matcher(body);
473:
474: if (!matcher.find()) {
475: return body;
476: }
477:
478: StringBuffer modifiedBody = new StringBuffer(body.length());
479: File mimePartFile;
480: List mimeParts = mimeTree.getAllLeafs();
481:
482: MimePart CIDPart = findMimePart(mimeParts, matcher.group(1));
483: if (CIDPart != null) {
484: mimePartFile = TempFileStore.createTempFile();
485: try {
486: downloadMimePart(CIDPart, mimePartFile);
487:
488: matcher.appendReplacement(modifiedBody, mimePartFile
489: .toURL().toString());
490: } catch (Exception e) {
491: matcher.appendReplacement(modifiedBody, "missing");
492: }
493: } else {
494: matcher.appendReplacement(modifiedBody, "missing");
495: }
496:
497: while (matcher.find()) {
498: CIDPart = findMimePart(mimeParts, matcher.group(1));
499: if (CIDPart != null) {
500: mimePartFile = TempFileStore.createTempFile();
501: try {
502: downloadMimePart(CIDPart, mimePartFile);
503:
504: matcher.appendReplacement(modifiedBody,
505: mimePartFile.toURL().toString());
506: } catch (Exception e) {
507: matcher.appendReplacement(modifiedBody, "missing");
508: }
509: } else {
510: matcher.appendReplacement(modifiedBody, "missing");
511: }
512: }
513:
514: matcher.appendTail(modifiedBody);
515:
516: return modifiedBody.toString();
517: }
518:
519: private MimePart findMimePart(List mimeParts, String findCid) {
520: MimePart result;
521: Iterator it = mimeParts.iterator();
522: while (it.hasNext()) {
523: result = (MimePart) it.next();
524:
525: String cid = result.getHeader().getContentID();
526: if (cid != null
527: && cid.substring(1, cid.length() - 1)
528: .equalsIgnoreCase(findCid)) {
529: return result;
530: }
531: }
532:
533: return null;
534: }
535:
536: private void downloadMimePart(MimePart part, File destFile)
537: throws Exception {
538: MimeHeader header = part.getHeader();
539:
540: InputStream bodyStream = folder.getMimePartBodyStream(uid, part
541: .getAddress());
542:
543: int encoding = header.getContentTransferEncoding();
544:
545: switch (encoding) {
546: case MimeHeader.QUOTED_PRINTABLE:
547: bodyStream = new QuotedPrintableDecoderInputStream(
548: bodyStream);
549: break;
550:
551: case MimeHeader.BASE64:
552: bodyStream = new Base64DecoderInputStream(bodyStream);
553: break;
554: default:
555: }
556:
557: FileOutputStream fileStream = new FileOutputStream(destFile);
558: StreamUtils.streamCopy(bodyStream, fileStream);
559: fileStream.close();
560: bodyStream.close();
561: }
562:
563: /**
564: * @return Returns the htmlMessage.
565: */
566: public boolean isHtmlMessage() {
567: return htmlMessage;
568: }
569:
570: protected URL extractURL(MouseEvent event) {
571: JEditorPane pane = (JEditorPane) event.getSource();
572: HTMLDocument doc = (HTMLDocument) pane.getDocument();
573:
574: Element e = doc.getCharacterElement(pane.viewToModel(event
575: .getPoint()));
576: AttributeSet a = e.getAttributes();
577: AttributeSet anchor = (AttributeSet) a.getAttribute(HTML.Tag.A);
578:
579: if (anchor == null) {
580: return null;
581: }
582:
583: URL url = null;
584:
585: try {
586: url = new URL((String) anchor
587: .getAttribute(HTML.Attribute.HREF));
588: } catch (MalformedURLException mue) {
589: return null;
590: }
591:
592: return url;
593: }
594:
595: /**
596: * this method extracts any url, but if URL's protocol is mailto: then this
597: * method also extracts the corresponding recipient name whatever it may be.
598: * <br>
599: * This "kind of" superseeds the previous extractURL(MouseEvent) method.
600: */
601: private ColumbaURL extractMailToURL(MouseEvent event) {
602:
603: ColumbaURL url = new ColumbaURL(extractURL(event));
604: if (url.getRealURL() == null)
605: return null;
606:
607: if (!url.getRealURL().getProtocol().equalsIgnoreCase("mailto"))
608: return url;
609:
610: JEditorPane pane = (JEditorPane) event.getSource();
611: HTMLDocument doc = (HTMLDocument) pane.getDocument();
612:
613: Element e = doc.getCharacterElement(pane.viewToModel(event
614: .getPoint()));
615: try {
616: url.setSender(doc.getText(e.getStartOffset(), (e
617: .getEndOffset() - e.getStartOffset())));
618: } catch (BadLocationException e1) {
619: url.setSender("");
620: }
621:
622: return url;
623: }
624:
625: class URLMouseListener implements MouseListener {
626:
627: public void mousePressed(MouseEvent event) {
628: if (event.isPopupTrigger()) {
629: processPopup(event);
630: }
631: }
632:
633: public void mouseReleased(MouseEvent event) {
634: if (event.isPopupTrigger()) {
635: processPopup(event);
636: }
637: }
638:
639: public void mouseEntered(MouseEvent event) {
640: }
641:
642: public void mouseExited(MouseEvent event) {
643: }
644:
645: public void mouseClicked(MouseEvent event) {
646: if (!SwingUtilities.isLeftMouseButton(event)) {
647: return;
648: }
649:
650: URL url = extractURL(event);
651:
652: if (url == null) {
653: return;
654: }
655:
656: if (url.getProtocol().equalsIgnoreCase("mailto")) {
657: // open composer
658: ComposerController controller = new ComposerController();
659: new DefaultContainer(controller);
660:
661: ComposerModel model = new ComposerModel();
662: model.setTo(url.getFile());
663:
664: // apply model
665: controller.setComposerModel(model);
666:
667: controller.updateComponents(true);
668: } else {
669: ColumbaDesktop.getInstance().browse(url);
670: }
671: }
672: }
673:
674: protected void processPopup(MouseEvent ev) {
675: // final URL url = extractURL(ev);
676: final ColumbaURL mailto = extractMailToURL(ev);
677:
678: final MouseEvent event = ev;
679: // open context-menu
680: // -> this has to happen in the awt-event dispatcher thread
681: SwingUtilities.invokeLater(new Runnable() {
682:
683: public void run() {
684: createPopupMenu(mailto).show(event.getComponent(),
685: event.getX(), event.getY());
686: }
687: });
688: }
689: }
|