/* * Created on Nov 9, 2004 * * This file is part of susimail project, see http://susi.i2p/ * * Copyright (C) 2004-2005 susi23@mail.i2p * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * $Revision: 1.2 $ */ package i2p.susi.webmail; import i2p.susi.util.Config; import i2p.susi.debug.Debug; import i2p.susi.util.ReadBuffer; import i2p.susi.webmail.encoding.DecodingException; import i2p.susi.webmail.encoding.Encoding; import i2p.susi.webmail.encoding.EncodingFactory; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.InputStreamReader; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import net.i2p.data.DataHelper; import net.i2p.util.SystemVersion; /** * data structure to hold a single message, mostly used with folder view and sorting * * @author susi */ class Mail { private static final String DATEFORMAT = "date.format"; private static final String unknown = "unknown"; private int size; public String sender, // as received, trimmed only, not HTML escaped reply, subject, dateString, //formattedSender, // address only, enclosed with <>, not HTML escaped formattedSubject, formattedDate, // US Locale, UTC localFormattedDate, // Current Locale, local time zone shortSender, // Either name or address but not both, HTML escaped, double-quotes removed, truncated with hellip shortSubject, // HTML escaped, truncated with hellip quotedDate; // Current Locale, local time zone, longer format public final String uidl; public Date date; private ReadBuffer header, body; private MailPart part; String[] to, cc; // addresses only, enclosed by <> private boolean isNew, isSpam; public String contentType; public String error; public boolean markForDeletion; public Mail(String uidl) { this.uidl = uidl; //formattedSender = unknown; formattedSubject = unknown; formattedDate = unknown; localFormattedDate = unknown; shortSender = unknown; shortSubject = unknown; quotedDate = unknown; error = ""; } /** * This may or may not contain the body also. * @return may be null */ public synchronized ReadBuffer getHeader() { return header; } public synchronized void setHeader(ReadBuffer rb) { if (rb == null) return; header = rb; parseHeaders(); } public synchronized boolean hasHeader() { return header != null; } /** * This contains the header also. * @return may be null */ public synchronized ReadBuffer getBody() { return body; } public synchronized void setBody(ReadBuffer rb) { if (rb == null) return; if (header == null) setHeader(rb); body = rb; size = rb.length; try { part = new MailPart(rb); } catch (DecodingException de) { Debug.debug(Debug.ERROR, "Decode error: " + de); } catch (RuntimeException e) { Debug.debug(Debug.ERROR, "Parse error: " + e); } } public synchronized boolean hasBody() { return body != null; } public synchronized MailPart getPart() { return part; } public synchronized boolean hasPart() { return part != null; } public synchronized int getSize() { return size; } public synchronized void setSize(int size) { if (body != null) return; this.size = size; } public synchronized boolean isSpam() { return isSpam; } public synchronized boolean isNew() { return isNew; } public synchronized void setNew(boolean isNew) { this.isNew = isNew; } public synchronized boolean hasAttachment() { // this isn't right but good enough to start // if part != null query parts instead? return contentType != null && !contentType.contains("text/plain") && !contentType.contains("multipart/alternative") && !contentType.contains("multipart/signed"); } /** * * @param address E-mail address to be validated * @return Is the e-mail address valid? */ public static boolean validateAddress( String address ) { if( address == null || address.length() == 0 ) return false; address = address.trim(); if( address.indexOf('\n') != -1 || address.indexOf('\r') != -1 ) return false; String[] tokens = DataHelper.split(address, "[ \t]+"); int addresses = 0; for( int i = 0; i < tokens.length; i++ ) { if( tokens[i].matches( "^[^@< \t]+@[^> \t]+$" ) || tokens[i].matches( "^<[^@< \t]+@[^> \t]+>$" ) ) addresses++; } return addresses == 1; } /** * Returns the first email address portion, enclosed by <> * @param address */ public static String getAddress(String address ) { String[] tokens = DataHelper.split(address, "[ \t]+"); for( int i = 0; i < tokens.length; i++ ) { if( tokens[i].matches( "^[^@< \t]+@[^> \t]+$" ) ) return "<" + tokens[i] + ">"; if( tokens[i].matches( "^<[^@< \t]+@[^> \t]+>$" ) ) return tokens[i]; } return null; } /** * A little misnamed. Adds all addresses from the comma-separated * line in text to the recipients list. * * @param text comma-separated * @param recipients out param * @param ok will be returned * @return true if ALL e-mail addresses are valid AND the in parameter was true */ public static boolean getRecipientsFromList( ArrayList<String> recipients, String text, boolean ok ) { if( text != null && text.length() > 0 ) { String[] ccs = DataHelper.split(text, ","); for( int i = 0; i < ccs.length; i++ ) { String recipient = ccs[i].trim(); if( validateAddress( recipient ) ) { String str = getAddress( recipient ); if( str != null && str.length() > 0 ) { recipients.add( str ); } else { ok = false; } } else { ok = false; } } } return ok; } /** * Adds all items from the list * to the builder, separated by tabs. * * @param buf out param * @param prefix prepended to the addresses */ public static void appendRecipients( StringBuilder buf, ArrayList<String> recipients, String prefix ) { for( String recipient : recipients ) { buf.append( prefix ); prefix ="\t"; buf.append( recipient ); buf.append( "\r\n" ); } } private void parseHeaders() { DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm"); DateFormat localDateFormatter = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT); DateFormat longLocalDateFormatter = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); // the router sets the JVM time zone to UTC but saves the original here so we can get it TimeZone tz = SystemVersion.getSystemTimeZone(); localDateFormatter.setTimeZone(tz); longLocalDateFormatter.setTimeZone(tz); DateFormat mailDateFormatter = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH ); error = ""; if( header != null ) { boolean ok = true; Encoding html = EncodingFactory.getEncoding( "HTML" ); if( html == null ) { error += "HTML encoder not found.\n"; ok = false; } Encoding hl = EncodingFactory.getEncoding( "HEADERLINE" ); if( hl == null ) { error += "Header line encoder not found.\n"; ok = false; } if( ok ) { try { ReadBuffer decoded = hl.decode( header ); BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( decoded.content, decoded.offset, decoded.length ), "UTF-8" ) ); String line; while( ( line = reader.readLine() ) != null ) { if( line.length() == 0 ) break; if( line.startsWith( "From:" ) ) { sender = line.substring( 5 ).trim(); //formattedSender = getAddress( sender ); shortSender = sender.replace("\"", "").trim(); int lt = shortSender.indexOf('<'); if (lt > 0) shortSender = shortSender.substring(0, lt).trim(); else if (lt < 0 && shortSender.contains("@")) shortSender = '<' + shortSender + '>'; // add missing <> (but thunderbird doesn't...) boolean trim = shortSender.length() > 40; if (trim) shortSender = shortSender.substring( 0, 37 ).trim(); shortSender = html.encode( shortSender ); if (trim) shortSender += "…"; // must be after html encode } else if( line.startsWith( "Date:" ) ) { dateString = line.substring( 5 ).trim(); try { date = mailDateFormatter.parse( dateString ); formattedDate = dateFormatter.format( date ); localFormattedDate = localDateFormatter.format( date ); //quotedDate = html.encode( dateString ); quotedDate = longLocalDateFormatter.format(date); } catch (ParseException e) { date = null; e.printStackTrace(); } } else if( line.startsWith( "Subject:" ) ) { subject = line.substring( 8 ).trim(); formattedSubject = subject; shortSubject = formattedSubject; boolean trim = formattedSubject.length() > 60; if (trim) shortSubject = formattedSubject.substring( 0, 57 ).trim(); shortSubject = html.encode( shortSubject ); if (trim) shortSubject += "…"; // must be after html encode } else if( line.toLowerCase(Locale.US).startsWith( "reply-to:" ) ) { reply = getAddress( line.substring( 9 ).trim() ); } else if( line.startsWith( "To:" ) ) { ArrayList<String> list = new ArrayList<String>(); getRecipientsFromList( list, line.substring( 3 ).trim(), true ); to = list.toArray(new String[list.size()]); } else if( line.startsWith( "Cc:" ) ) { ArrayList<String> list = new ArrayList<String>(); getRecipientsFromList( list, line.substring( 3 ).trim(), true ); cc = list.toArray(new String[list.size()]); } else if(line.equals( "X-Spam-Flag: YES" )) { // TODO trust.spam.headers config isSpam = true; } else if(line.toLowerCase(Locale.US).startsWith("content-type:" )) { // this is duplicated in MailPart but // we want to know if we have attachments, even if // we haven't fetched the body contentType = line.substring(13).trim(); } } } catch( Exception e ) { error += "Error parsing mail header: " + e.getClass().getName() + '\n'; } } } } }