5 import org.ibex.mail.protocol.*;
6 import org.ibex.util.*;
12 import java.sql.Timestamp;
13 import java.sql.Connection;
15 public class Whitelist extends SqliteDB {
17 public Whitelist(String filename) throws SQLException {
19 SqliteTable whitelist = getTable("whitelist", "(email)");
20 whitelist.createIndex("email");
21 SqliteTable pending = getTable("pending", "(spamid,email,message,date)");
23 pending.createIndex("spamid");
24 pending.createIndex("email");
27 public boolean handleRequest(org.ibex.net.Connection c) {
29 Socket sock = c.getSocket();
30 BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
31 String s = br.readLine();
32 String url = s.substring(s.indexOf(' ')+1);
33 url = url.substring(0, url.indexOf(' '));
34 while(s!=null && !s.equals(""))
36 PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
37 if (!url.startsWith("/whitelist/")) {
38 pw.print("HTTP/1.0 404 Not Found\r\n");
39 pw.print("Content-Type: text/plain\r\n");
41 pw.println("you are lost.");
43 url = url.substring("/whitelist/".length());
44 url = URLDecoder.decode(url);
45 if (url.endsWith(".txt")) url = url.substring(0, url.length()-4);
46 pw.print("HTTP/1.0 200 OK\r\n");
47 pw.print("Content-Type: text/plain\r\n");
50 SMTP.whitelist.response(url);
51 pw.println("Thanks! You've been added to my list of non-spammers and your message");
52 pw.println("has been moved to my inbox.");
53 pw.println("email id " + url);
55 } catch (Exception e) {
56 e.printStackTrace(pw);
61 } catch (Exception e) { throw new RuntimeException(e); }
65 public synchronized boolean isWhitelisted(Address a) {
67 if (a==null) return false;
68 PreparedStatement check = conn.prepareStatement("select * from 'whitelist' where email=?");
69 check.setString(1, a.toString(false).toLowerCase());
70 ResultSet rs = check.executeQuery();
71 return !rs.isAfterLast();
72 } catch (SQLException e) { throw new RuntimeException(e); }
75 public synchronized void addWhitelist(Address a) {
77 PreparedStatement add = conn.prepareStatement("insert or replace into 'whitelist' values(?)");
78 add.setString(1, a.toString(false).toLowerCase());
80 } catch (SQLException e) { throw new RuntimeException(e); }
83 public synchronized void response(String messageid) throws IOException, MailException {
85 PreparedStatement query = conn.prepareStatement("select email,message from pending where spamid=?");
86 query.setString(1, messageid);
87 ResultSet rs = query.executeQuery();
89 throw new RuntimeException("could not find messageid \""+messageid+"\"");
90 HashSet<Message> hsm = new HashSet<Message>();
93 addWhitelist(Address.parse(rs.getString(1)));
94 Message m = Message.newMessage(new Fountain.StringFountain(rs.getString(2)));
95 Address a = m.headers.get("reply-to")==null ? null : Address.parse(m.headers.get("reply-to"));
96 if (a!=null) addWhitelist(a);
98 if (a!=null) addWhitelist(a);
100 if (a!=null) addWhitelist(a);
102 if (m.cc != null) for(Address aa : m.cc) {
103 if (aa!= null) addWhitelist(aa);
108 Target.root.accept(m);
109 } catch (SQLException e) { throw new RuntimeException(e); }
112 public void challenge(Message m) {
114 // FIXME: don't challenge emails with binaries in them;
115 // reject them outright and have the sender send an
116 // initial message w/o a binary.
118 // FIXME: use Auto here!!!
119 // The challenge should refer to the message-id of the mail being challenged.
121 // FIXME: watch outgoing MessageID's: if something comes
122 // back with an In-Reply-To mentioning a MessageID from
123 // the last few days, auto-whitelist them.
125 // FIXME: important that "From" on the challenge matches
126 // RCPT TO on the original message.
128 Log.warn(Whitelist.class, "challenging message: " + m.summary());
130 Address to = m.headers.get("reply-to")==null ? null : Address.parse(m.headers.get("reply-to"));
131 if (to==null) to = m.from;
132 if (to==null) to = m.envelopeFrom;
134 if (m.envelopeTo==null || m.envelopeTo.equals("null") || m.envelopeTo.equals("")) {
135 Log.warn(this, "message is missing a to/replyto/envelopeto header; cannot accept");
138 if (m.headers.get("Auto-Submitted") != null &&
139 m.headers.get("Auto-Submitted").toLowerCase().indexOf("auto-replied")!=-1) {
140 Log.warn(this, "refusing to send a challenge to a message "+
141 "with Auto-Submitted=\""+m.headers.get("Auto-Submitted")+"\"");
145 Address from = Address.parse("adam@megacz.com");
147 String messageid = "x" + m.messageid.substring(1);
148 messageid = messageid.substring(0, messageid.length() - 1);
149 messageid = messageid.replace('%','_');
150 Log.warn(Whitelist.class, "got challenge for: " + messageid);
152 String url = "http://www.megacz.com:8025/whitelist/"+URLEncoder.encode(messageid)+".txt";
154 "Return-Path: <>" + "\r\n" +
155 "Envelope-To: " + to + "\r\n" +
156 "X-Originally-Received: " + m.headers.get("received") + "\r\n" +
157 "To: " + to + "\r\n" +
158 "From: " + from + "\r\n" +
159 "Subject: Re: " + m.subject + "\r\n" +
160 "Message-ID:" + Message.generateFreshMessageId() + "\r\n" +
162 "Hi, I've never sent a message to you before, so my spam filter trapped\n" +
163 "your email. If you're really a human being and not an evil spammer,\n" +
164 "please click the link below or paste it into a web browser; doing so will\n" +
165 "add you to my list of non-spammers (so you won't get this email in the future)\n"+
166 "and it will move your message from my spam folder to my incoming mail folder.\n" +
175 "About this message:\n" +
177 "NOTE: SPAMCOP DOES NOT CONSIDER THIS TO BE SPAM; see this:\n"+
179 " http://www.spamcop.net/fom-serve/cache/369.html\n"+
181 " and examine the \"x-originally-received\" header on this message \n"+
182 " for the required \"chain of custody\" information.\n"+
184 " Only one of these challenge messages is ever generated in response to \n"+
185 " a given inbound SMTP connection; it cannot be used to amplify spam \n"+
186 " attacks, and in fact actually retards them while also stripping the \n"+
187 " advertisement they were meant to convey.\n"+
189 " Only one delivery attempt for this challenge is ever made, and it is made\n"+
190 " DURING the SMTP delivery of the message being challenged (that is, \n"+
191 " between C:DATA and S:250); the deliverer of the possibly-spam message\n"+
192 " must remain SMTP connected to my server during the entire process or else\n"+
193 " the delivery will immediately abort. These challenge messages are NEVER,\n"+
194 " EVER queued for multiple delivery attempts\n"+
196 " For more information, please see:\n"+
198 " http://www.templetons.com/brad/spam/crgood.html\n";
200 Message challenge = Message.newMessage(new Fountain.StringFountain(message));
202 boolean send = false;
204 PreparedStatement query = conn.prepareStatement("select email from pending where email=?");
205 query.setString(1, to.toString(false));
206 ResultSet rs = query.executeQuery();
208 Log.warn(this, "already challenged " + to.toString(false) + "; not challenging again.");
215 if (!SMTP.Outgoing.attempt(challenge))
216 throw new RuntimeException("attempted to send challenge but could not: " + m.summary());
219 PreparedStatement add = conn.prepareStatement("insert into pending values(?,?,?,?)");
220 add.setString(1, messageid);
221 add.setString(2, to.toString(false));
222 add.setString(3, SqliteDB.streamToString(m.getStream()));
223 add.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
226 } catch (Exception e) { throw new RuntimeException(e); }