conflict merge, bugfixes
[org.ibex.mail.git] / src / org / ibex / mail / Message.java
1 package org.ibex.mail;
2 import org.ibex.crypto.*;
3 import org.ibex.js.*;
4 import org.ibex.util.*;
5 import org.ibex.mail.protocol.*;
6 import java.util.*;
7 import java.net.*;
8 import java.io.*;
9
10 // FIXME MIME: RFC2045, 2046, 2049
11 // NOTE: always use Win32 line endings
12 // hard line limit: 998 chars
13 // soft line limit (suggested): 78 chars
14 // header fields: ascii 33-126 (but no colon)
15 // field body: anything ASCII except CRLF
16 // folded headers: can insert CRLF anywhere that whitespace appears (before the whitespace)
17 // body needs CRLF; one or the other alone is not acceptable
18 // date/time parsing: see 3.3
19
20 // FEATURE: PGP-signature-parsing
21 // FEATURE: mailing list header parsing
22 // FEATURE: delivery status notification (and the sneaky variety)
23 // FEATURE: threading as in http://www.jwz.org/doc/threading.html
24
25 public class Message extends JSReflection {
26
27     public final String allHeaders;   // pristine headers
28     public final Hashtable headers;   // hash of headers (not including resent's and traces)
29     public final String body;         // entire body
30
31     // parsed header fields
32     public final Date date;
33     public final Address to;
34     public final Address from;        // if multiple From entries, this is sender
35     public final Address replyto;     // if none provided, this is equal to sender
36     public final String subject;
37     public final String messageid;
38     public final Address[] cc;
39     public final Address[] bcc;
40     public final Hashtable[] resent;
41     public final Trace[] traces;
42
43     // envelope fields
44     public final Address envelopeFrom;
45     public final Address[] envelopeTo;
46
47     public void dump(OutputStream os) {
48         Log.error(this, "not implemented");
49     }
50
51     public static class StoredMessage extends Message {
52         public StoredMessage(/*ReadStream rs*/SMTP.LineReader rs, boolean dotTerminatedLikeSMTP) throws IOException {
53             super(rs, dotTerminatedLikeSMTP); uid = -1; }
54         public final int uid;
55         public boolean deleted = false;
56         public boolean read = false;
57         public boolean answered = false;
58         public String dumpStoredForm() { throw new Error("StoredMessage.dumpStoredForm() not implemented"); };
59         public static StoredMessage undump(InputStream os) {
60             Log.error(StoredMessage.class, "not implemented");
61             return null;
62         }
63     }
64
65     public static class Address extends JSReflection {
66         public String coerceToString() { return toString(); }
67         public String toString() {
68             if (description == null || description.equals("")) return user +"@"+ host;
69             return description + " " + "<" + user +"@"+ host + ">";
70         }
71         public final String user;
72         public final String host;
73         public final String description;
74         public Address(String user, String host, String description) {
75             this.user = user; this.host = host; this.description = description;
76         }
77         public Address(String s) {
78             s = s.trim();
79             String descrip = null;
80             if (s.indexOf('<') != -1) {
81                 if (s.indexOf('>') == -1) { /* FIXME */ }
82                 descrip = s.substring(0, s.indexOf('<')) + s.substring(s.indexOf('>') + 1);
83                 s = s.substring(s.indexOf('<') + 1, s.indexOf('>'));
84             }
85             if (s.indexOf('@') == -1) { /* FIXME */ }
86             description = descrip;
87             user = s.substring(0, s.indexOf('@'));
88             host = s.substring(s.indexOf('@')+1);
89         }
90     }
91
92     public class Trace {
93         String returnPath = null;
94         Element[] elements;
95         public class Element {
96             // FIXME final
97             String fromDomain;
98             String fromIP;
99             String toDomain;
100             String forWhom;
101             Date date;
102         }
103     }
104
105     // FIXME: support dotTerminatedLikeSMTP
106     public Message(/*ReadStream rs*/SMTP.LineReader rs, boolean dotTerminatedLikeSMTP) throws IOException {
107         String key = null;
108         StringBuffer all = new StringBuffer();
109         String lastKey = null;
110         replyto = null;
111         subject = null;
112         messageid = null;
113         cc = null;
114         bcc = null;
115         resent = null;
116         traces = null;
117         envelopeFrom = null;
118         envelopeTo = null;
119
120         headers = new Hashtable();
121         date = null; // FIXME
122         to = null;
123         from = null;
124         for(String s = rs.readLine(); s != null && !s.equals(""); s = rs.readLine()) {
125             all.append(s);
126             all.append("\r\n");
127             if (Character.isSpace(s.charAt(0))) {
128                 if (lastKey == null) { /* FIXME */ }
129                 headers.put(lastKey, headers.get(lastKey) + s);
130                 continue;
131             }
132             if (s.indexOf(':') == -1) {
133                 /* FIXME */
134                 break;
135             }
136
137             lastKey = key = s.substring(0, s.indexOf(':'));
138             String val = s.substring(0, s.indexOf(':') + 1);
139             while(Character.isSpace(val.charAt(0))) val = val.substring(1);
140
141             if (headers.get(key) != null)
142                 if (key.startsWith("Resent-")) {
143                     // FIXME: multi-resent headers
144                 } else if (key.startsWith("Return-Path:")) {
145                     // FIXME: parse traces, see RFC2821, section 4.4                    
146                 } else if (key.startsWith("Recieved:")) {
147                     // FIXME: parse traces, see RFC2821, section 4.4                    
148                 } else {
149                     // just append it to the previous one; valid for Comments/Keywords
150                     val = headers.get(key) + " " + val;
151                 } 
152             
153             headers.put(key, val);
154         }
155
156         allHeaders = all.toString();
157
158         StringBuffer body = new StringBuffer();
159         for(String s = rs.readLine();; s = rs.readLine()) {
160             if (s == null || (dotTerminatedLikeSMTP && s.equals("."))) break;
161             body.append(s);
162         }
163
164         this.body = body.toString();
165     }
166
167     // http://www.jwz.org/doc/mid.html
168     public static String generateFreshMessageId() {
169         StringBuffer ret = new StringBuffer();
170         ret.append('<');
171         ret.append(Base36.encode(System.currentTimeMillis()));
172         ret.append('.');
173         ret.append(Base36.encode(random.nextLong()));
174         ret.append('.');
175         try { ret.append(InetAddress.getLocalHost().getHostName()); } catch (UnknownHostException e) { /* DELIBERATE */ }
176         ret.append('>');
177         return ret.toString();
178     }
179
180     private static final Random random = new Random();
181 }