don't challenge the same sender twice
[org.ibex.mail.git] / src / org / ibex / mail / Whitelist.java
1 package org.ibex.mail;
2
3 import org.ibex.io.*;
4 import org.ibex.mail.protocol.*;
5 import org.ibex.util.*;
6 import java.sql.*;
7 import java.net.*;
8 import java.io.*;
9 import java.util.*;
10 import java.sql.Timestamp;
11 import java.sql.Connection;
12
13 // now all I need is the click-through page
14
15 // FIXME: periodic cleanup
16 public class Whitelist {
17
18     private Connection conn;
19
20     // FIXME very ugly
21     static {
22         new Thread() { public void run() { startWebServer(); } }.start();
23     }
24     public static void startWebServer() {
25         try {
26             ServerSocket ss = new ServerSocket(8025);
27             while(true) {
28                 final Socket sock = ss.accept();
29                 new Thread() {
30                     public void run() {
31                         try {
32                             BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
33                             String s = br.readLine();
34                             String url = s.substring(s.indexOf(' ')+1);
35                             url = url.substring(0, url.indexOf(' '));
36                             while(s!=null && !s.equals(""))
37                                 s = br.readLine();
38                             PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
39                             if (!url.startsWith("/whitelist/")) {
40                                 pw.print("HTTP/1.0 404 Not FoundK\r\n");
41                                 pw.print("Content-Type: text/plain\r\n");
42                                 pw.print("\r\n");
43                                 pw.println("you are lost.");
44                             } else {
45                                 url = url.substring("/whitelist/".length());
46                                 url = URLDecoder.decode(url);
47                                 pw.print("HTTP/1.0 200 OK\r\n");
48                                 pw.print("Content-Type: text/plain\r\n");
49                                 pw.print("\r\n");
50                                 try {
51                                     SMTP.whitelist.response(url);
52                                     pw.println("Thanks!  You've been added to my list of non-spammers and your message");
53                                     pw.println("has been moved to my inbox.");
54                                     pw.println("email id " + url);
55                                     pw.println("");
56                                 } catch (Exception e) {
57                                     e.printStackTrace(pw);
58                                 }
59                             }
60                             pw.flush();
61                             sock.close();
62                         } catch (Exception e) { throw new RuntimeException(e); }
63                     }
64                 }.start();
65             }
66         } catch (Exception e) { throw new RuntimeException(e); }
67     }
68
69
70     public Whitelist(String filename) {
71         try {
72             Class.forName("org.sqlite.JDBC");
73             conn = DriverManager.getConnection("jdbc:sqlite:"+filename);
74             conn.prepareStatement("create table if not exists "+
75                                   "'whitelist' (email)").executeUpdate();
76             conn.prepareStatement("create table if not exists "+
77                                   "'pending' (spamid,email,message,date)").executeUpdate();
78         }
79         catch (SQLException e) { throw new RuntimeException(e); }
80         catch (ClassNotFoundException e) { throw new RuntimeException(e); }
81     }
82
83     public synchronized boolean isWhitelisted(Address a) {
84         try {
85             PreparedStatement check = conn.prepareStatement("select * from 'whitelist' where email=?");
86             check.setString(1, a.toString(false).toLowerCase());
87             ResultSet rs = check.executeQuery();
88             return !rs.isAfterLast();
89         } catch (SQLException e) { throw new RuntimeException(e); }
90     }
91
92     public synchronized void addWhitelist(Address a) {
93         try {
94             PreparedStatement add    = conn.prepareStatement("insert or replace into 'whitelist' values(?)");
95             add.setString(1, a.toString(false).toLowerCase());
96             add.executeUpdate();
97         } catch (SQLException e) { throw new RuntimeException(e); }
98     }
99
100     public synchronized void response(String messageid) throws IOException, MailException {
101         try {
102             PreparedStatement query = conn.prepareStatement("select email,message from pending where spamid=?");
103             query.setString(1, messageid);
104             ResultSet rs = query.executeQuery();
105             if (!rs.next())
106                 throw new RuntimeException("could not find messageid \""+messageid+"\"");
107             do {
108                 addWhitelist(Address.parse(rs.getString(1)));
109                 Message m = Message.newMessage(new Fountain.StringFountain(rs.getString(2)));
110                 Target.root.accept(m);
111             } while (rs.next());
112         } catch (SQLException e) { throw new RuntimeException(e); }
113     }
114
115     public synchronized void challenge(Message m) {
116         try {
117             Log.warn(Whitelist.class, "challenging message: " + m.summary());
118
119             Address to = m.headers.get("reply-to")==null ? null : Address.parse(m.headers.get("reply-to"));
120             if (to==null) to = m.from;
121             if (to==null) to = m.envelopeFrom;
122
123             if (m.envelopeTo==null || m.envelopeTo.equals("null") || m.envelopeTo.equals("")) {
124                 Log.warn(this, "message is missing a to/replyto/envelopeto header; cannot accept");
125                 return;
126             }
127             if (m.headers.get("Auto-Submitted") != null &&
128                 m.headers.get("Auto-Submitted").toLowerCase().indexOf("auto-replied")!=-1) {
129                 Log.warn(this, "refusing to send a challenge to a message "+
130                          "with Auto-Submitted=\""+m.headers.get("Auto-Submitted")+"\"");
131                 return;
132             }
133
134             Address from = Address.parse("adam@megacz.com");
135
136             String messageid = "x" + m.messageid.substring(1);
137             messageid = messageid.substring(0, messageid.length() - 1);
138             messageid = messageid.replace('%','_');
139             Log.warn(Whitelist.class, "got challenge for: " + messageid);
140
141             String url = "http://www.megacz.com:8025/whitelist/"+URLEncoder.encode(messageid);
142             String message =
143                 "Return-Path: <>"                                     + "\r\n" +
144                 "Envelope-To: "           + to                        + "\r\n" +
145                 "X-Originally-Received: " + m.headers.get("received") + "\r\n" +
146                 "To: "                    + to                        + "\r\n" +
147                 "From: "                  + from                      + "\r\n" +
148                 "Subject: Re: "           + m.subject                 + "\r\n" +
149                 "Message-ID:"             + Message.generateFreshMessageId() + "\r\n" +
150                 "\r\n" +
151                 "Hi, I've never sent a message to you before, so my spam filter trapped\n"        +
152                 "your email.  If you're really a human being and not an evil spammer,\n"          +
153                 "please click the link below or paste it into a web browser; doing so will\n"     +
154                 "add you to my list of non-spammers (so you won't get this email in the future)\n"+
155                 "and it will move your message from my spam folder to my incoming mail folder.\n" +
156                 "\n"                                                                              +
157                 "Thanks!\n"                                                                       +
158                 "\n"                                                                              +
159                 "  - Adam\n"                                                                      +
160                 "\n"                                                                              +
161                 url+"\n" +
162                 "\n"                                                                              +
163                 "\n"                                                                              +
164                 "About this message:\n" +
165                 "\n"                                                                              +
166                 "NOTE: SPAMCOP DOES NOT CONSIDER THIS TO BE SPAM; see this:\n"+
167                 "\n"+
168                 "         http://www.spamcop.net/fom-serve/cache/369.html\n"+
169                 "\n"+
170                 "      and examine the \"x-originally-received\" header on this message \n"+
171                 "      for the required \"chain of custody\" information.\n"+
172                 "\n"+
173                 "      Only one of these challenge messages is ever generated in response to \n"+
174                 "      a given inbound SMTP connection; it cannot be used to amplify spam    \n"+
175                 "      attacks, and in fact actually retards them while also stripping the   \n"+
176                 "      advertisement they were meant to convey.\n"+
177                 "\n"+
178                 "      Only one delivery attempt for this challenge is ever made, and it is made\n"+
179                 "      DURING the SMTP delivery of the message being challenged (that is, \n"+
180                 "      between C:DATA and S:250); the deliverer of the possibly-spam message\n"+
181                 "      must remain SMTP connected to my server during the entire process or else\n"+
182                 "      the delivery will immediately abort.  These challenge messages are NEVER,\n"+
183                 "      EVER queued for multiple delivery attempts\n"+
184                 "      \n"+
185                 "      For more information, please see:\n"+
186                 "      \n"+
187                 "      http://www.templetons.com/brad/spam/crgood.html\n";
188
189             Message challenge = Message.newMessage(new Fountain.StringFountain(message));
190
191             PreparedStatement query = conn.prepareStatement("select email from pending where email=?");
192             query.setString(1, to.toString(false));
193             ResultSet rs = query.executeQuery();
194             if (rs.next()) {
195                 Log.warn(this, "already challenged " + to.toString(false) + "; not challenging again.");
196             } else {
197                 if (!SMTP.Outgoing.attempt(challenge))
198                     throw new RuntimeException("attempted to send challenge but could not: " + m.summary());
199             }
200
201             PreparedStatement add = conn.prepareStatement("insert into pending values(?,?,?,?)");
202             add.setString(1, messageid);
203             add.setString(2, to.toString(false));
204             add.setString(3, streamToString(m.getStream()));
205             add.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
206             add.executeUpdate();
207         } catch (Exception e) { throw new RuntimeException(e); }
208     }
209
210     private static String streamToString(Stream stream) throws Exception {
211         StringBuffer b = new StringBuffer();
212         for(String s = stream.readln(); s!=null; s=stream.readln())
213             b.append(s+"\n");
214         return b.toString();
215     }
216 }
217