--- /dev/null
+package org.ibex.mail;
+
+import org.ibex.io.*;
+import org.ibex.mail.protocol.*;
+import org.ibex.util.*;
+import java.sql.*;
+import java.net.*;
+import java.io.*;
+import java.util.*;
+import java.sql.Timestamp;
+import java.sql.Connection;
+
+// now all I need is the click-through page
+
+// FIXME: periodic cleanup
+public class Whitelist {
+
+ private Connection conn;
+
+ // FIXME very ugly
+ static {
+ new Thread() { public void run() { startWebServer(); } }.start();
+ }
+ public static void startWebServer() {
+ try {
+ ServerSocket ss = new ServerSocket(8025);
+ while(true) {
+ final Socket sock = ss.accept();
+ new Thread() {
+ public void run() {
+ try {
+ BufferedReader br = new BufferedReader(new InputStreamReader(sock.getInputStream()));
+ String s = br.readLine();
+ String url = s.substring(s.indexOf(' ')+1);
+ url = url.substring(0, url.indexOf(' '));
+ while(s!=null && !s.equals(""))
+ s = br.readLine();
+ PrintWriter pw = new PrintWriter(new OutputStreamWriter(sock.getOutputStream()));
+ if (!url.startsWith("/whitelist/")) {
+ pw.print("HTTP/1.0 404 Not FoundK\r\n");
+ pw.print("Content-Type: text/plain\r\n");
+ pw.print("\r\n");
+ pw.println("you are lost.");
+ } else {
+ url = url.substring("/whitelist/".length());
+ url = URLDecoder.decode(url);
+ pw.print("HTTP/1.0 200 OK\r\n");
+ pw.print("Content-Type: text/plain\r\n");
+ pw.print("\r\n");
+ try {
+ SMTP.whitelist.response(url);
+ pw.println("Thanks! You've been added to my list of non-spammers and your message");
+ pw.println("has been moved to my inbox.");
+ pw.println("email id " + url);
+ pw.println("");
+ } catch (Exception e) {
+ e.printStackTrace(pw);
+ }
+ }
+ pw.flush();
+ sock.close();
+ } catch (Exception e) { throw new RuntimeException(e); }
+ }
+ }.start();
+ }
+ } catch (Exception e) { throw new RuntimeException(e); }
+ }
+
+
+ public Whitelist(String filename) {
+ try {
+ Class.forName("org.sqlite.JDBC");
+ conn = DriverManager.getConnection("jdbc:sqlite:"+filename);
+ conn.prepareStatement("create table if not exists "+
+ "'whitelist' (email)").executeUpdate();
+ conn.prepareStatement("create table if not exists "+
+ "'pending' (spamid,email,message,date)").executeUpdate();
+ }
+ catch (SQLException e) { throw new RuntimeException(e); }
+ catch (ClassNotFoundException e) { throw new RuntimeException(e); }
+ }
+
+ public synchronized boolean isWhitelisted(Address a) {
+ try {
+ PreparedStatement check = conn.prepareStatement("select * from 'whitelist' where email=?");
+ check.setString(1, a.toString(false).toLowerCase());
+ ResultSet rs = check.executeQuery();
+ return !rs.isAfterLast();
+ } catch (SQLException e) { throw new RuntimeException(e); }
+ }
+
+ public synchronized void addWhitelist(Address a) {
+ try {
+ PreparedStatement add = conn.prepareStatement("insert or replace into 'whitelist' values(?)");
+ add.setString(1, a.toString(false).toLowerCase());
+ add.executeUpdate();
+ } catch (SQLException e) { throw new RuntimeException(e); }
+ }
+
+ public synchronized void response(String messageid) throws IOException, MailException {
+ try {
+ PreparedStatement query = conn.prepareStatement("select email,message from pending where spamid=?");
+ query.setString(1, messageid);
+ ResultSet rs = query.executeQuery();
+ if (!rs.next())
+ throw new RuntimeException("could not find messageid \""+messageid+"\"");
+ do {
+ addWhitelist(Address.parse(rs.getString(1)));
+ Message m = Message.newMessage(new Fountain.StringFountain(rs.getString(2)));
+ Target.root.accept(m);
+ } while (rs.next());
+ } catch (SQLException e) { throw new RuntimeException(e); }
+ }
+
+ public synchronized void challenge(Message m) {
+ try {
+ Log.warn(Whitelist.class, "challenging message: " + m.summary());
+ Address to = m.headers.get("reply-to")==null ? null : Address.parse(m.headers.get("reply-to"));
+ if (to==null) to = m.from;
+ if (to==null) to = m.envelopeFrom;
+
+ // FIXME
+ //if (to==null) return ibex.mail.drop("message is missing a to/replyto/envelopeto header; cannot accept");
+
+ Address from = Address.parse("adam@megacz.com");
+
+ String messageid = "x" + m.messageid.substring(1);
+ messageid = messageid.substring(0, messageid.length() - 1);
+ messageid = messageid.replace('%','_');
+ Log.warn(Whitelist.class, "got challenge for: " + messageid);
+
+ String url = "http://www.megacz.com:8025/whitelist/"+URLEncoder.encode(messageid);
+ String message =
+ "Return-Path: <>" + "\r\n" +
+ "Envelope-To: " + to + "\r\n" +
+ "X-Originally-Received: " + m.headers.get("received") + "\r\n" +
+ "To: " + to + "\r\n" +
+ "From: " + from + "\r\n" +
+ "Subject: Re: " + m.subject + "\r\n" +
+ "Message-ID:" + Message.generateFreshMessageId() + "\r\n" +
+ "\r\n" +
+ "Hi, I've never sent a message to you before, so my spam filter trapped\n" +
+ "your email. If you're really a human being and not an evil spammer,\n" +
+ "please click the link below or paste it into a web browser; doing so will\n" +
+ "add you to my list of non-spammers (so you won't get this email in the future)\n"+
+ "and it will move your message from my spam folder to my incoming mail folder.\n" +
+ "\n" +
+ "Thanks!\n" +
+ "\n" +
+ " - Adam\n" +
+ "\n" +
+ url+"\n" +
+ "\n" +
+ "\n" +
+ "About this message:\n" +
+ "\n" +
+ "NOTE: SPAMCOP DOES NOT CONSIDER THIS TO BE SPAM; see this:\n"+
+ "\n"+
+ " http://www.spamcop.net/fom-serve/cache/369.html\n"+
+ "\n"+
+ " and examine the \"x-originally-received\" header on this message \n"+
+ " for the required \"chain of custody\" information.\n"+
+ "\n"+
+ " Only one of these challenge messages is ever generated in response to \n"+
+ " a given inbound SMTP connection; it cannot be used to amplify spam \n"+
+ " attacks, and in fact actually retards them while also stripping the \n"+
+ " advertisement they were meant to convey.\n"+
+ "\n"+
+ " Only one delivery attempt for this challenge is ever made, and it is made\n"+
+ " DURING the SMTP delivery of the message being challenged (that is, \n"+
+ " between C:DATA and S:250); the deliverer of the possibly-spam message\n"+
+ " must remain SMTP connected to my server during the entire process or else\n"+
+ " the delivery will immediately abort. These challenge messages are NEVER,\n"+
+ " EVER queued for multiple delivery attempts\n"+
+ " \n"+
+ " For more information, please see:\n"+
+ " \n"+
+ " http://www.templetons.com/brad/spam/crgood.html\n";
+
+ Message challenge = Message.newMessage(new Fountain.StringFountain(message));
+ if (!SMTP.Outgoing.attempt(challenge))
+ throw new RuntimeException("attempted to send challenge but could not: " + m.summary());
+
+ PreparedStatement add = conn.prepareStatement("insert into pending values(?,?,?,?)");
+ add.setString(1, messageid);
+ add.setString(2, to.toString(false));
+ add.setString(3, streamToString(m.getStream()));
+ add.setTimestamp(4, new Timestamp(System.currentTimeMillis()));
+ add.executeUpdate();
+ } catch (Exception e) { throw new RuntimeException(e); }
+ }
+
+ private static String streamToString(Stream stream) throws Exception {
+ StringBuffer b = new StringBuffer();
+ for(String s = stream.readln(); s!=null; s=stream.readln())
+ b.append(s+"\n");
+ return b.toString();
+ }
+}
+