4 import org.ibex.mail.protocol.*;
5 import org.ibex.util.*;
10 import java.sql.Timestamp;
11 import java.sql.Connection;
13 // now all I need is the click-through page
15 // FIXME: periodic cleanup
16 public class Whitelist {
18 private Connection conn;
22 new Thread() { public void run() { startWebServer(); } }.start();
24 public static void startWebServer() {
26 ServerSocket ss = new ServerSocket(8025);
28 final Socket sock = ss.accept();
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(""))
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");
43 pw.println("you are lost.");
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");
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);
56 } catch (Exception e) {
57 e.printStackTrace(pw);
62 } catch (Exception e) { throw new RuntimeException(e); }
66 } catch (Exception e) { throw new RuntimeException(e); }
70 public Whitelist(String filename) {
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();
79 catch (SQLException e) { throw new RuntimeException(e); }
80 catch (ClassNotFoundException e) { throw new RuntimeException(e); }
83 public synchronized boolean isWhitelisted(Address a) {
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); }
92 public synchronized void addWhitelist(Address a) {
94 PreparedStatement add = conn.prepareStatement("insert or replace into 'whitelist' values(?)");
95 add.setString(1, a.toString(false).toLowerCase());
97 } catch (SQLException e) { throw new RuntimeException(e); }
100 public synchronized void response(String messageid) throws IOException, MailException {
102 PreparedStatement query = conn.prepareStatement("select email,message from pending where spamid=?");
103 query.setString(1, messageid);
104 ResultSet rs = query.executeQuery();
106 throw new RuntimeException("could not find messageid \""+messageid+"\"");
108 addWhitelist(Address.parse(rs.getString(1)));
109 Message m = Message.newMessage(new Fountain.StringFountain(rs.getString(2)));
110 Target.root.accept(m);
112 } catch (SQLException e) { throw new RuntimeException(e); }
115 public synchronized void challenge(Message m) {
117 Log.warn(Whitelist.class, "challenging message: " + m.summary());
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;
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");
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")+"\"");
134 Address from = Address.parse("adam@megacz.com");
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);
141 String url = "http://www.megacz.com:8025/whitelist/"+URLEncoder.encode(messageid);
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" +
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" +
164 "About this message:\n" +
166 "NOTE: SPAMCOP DOES NOT CONSIDER THIS TO BE SPAM; see this:\n"+
168 " http://www.spamcop.net/fom-serve/cache/369.html\n"+
170 " and examine the \"x-originally-received\" header on this message \n"+
171 " for the required \"chain of custody\" information.\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"+
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"+
185 " For more information, please see:\n"+
187 " http://www.templetons.com/brad/spam/crgood.html\n";
189 Message challenge = Message.newMessage(new Fountain.StringFountain(message));
191 PreparedStatement query = conn.prepareStatement("select email from pending where email=?");
192 query.setString(1, to.toString(false));
193 ResultSet rs = query.executeQuery();
195 Log.warn(this, "already challenged " + to.toString(false) + "; not challenging again.");
197 if (!SMTP.Outgoing.attempt(challenge))
198 throw new RuntimeException("attempted to send challenge but could not: " + m.summary());
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()));
207 } catch (Exception e) { throw new RuntimeException(e); }
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())