add whitelisting code
authoradam <adam@megacz.com>
Sat, 20 Jan 2007 22:29:33 +0000 (22:29 +0000)
committeradam <adam@megacz.com>
Sat, 20 Jan 2007 22:29:33 +0000 (22:29 +0000)
darcs-hash:20070120222933-5007d-9623d8533ef5f1705be241ed8694d1a96ffb35a7.gz

src/org/ibex/mail/Whitelist.java [new file with mode: 0644]
src/org/ibex/mail/protocol/SMTP.java
src/org/ibex/mail/target/Script.java
src/org/ibex/mail/target/SqliteJdbcMailbox.java

diff --git a/src/org/ibex/mail/Whitelist.java b/src/org/ibex/mail/Whitelist.java
new file mode 100644 (file)
index 0000000..c8f54ea
--- /dev/null
@@ -0,0 +1,200 @@
+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();
+    }
+}
+
index bca478b..37686c6 100644 (file)
@@ -36,6 +36,9 @@ public class SMTP {
     public static final Graylist graylist =
         new Graylist(Mailbox.STORAGE_ROOT+"/db/graylist.sqlite");
 
+    public static final Whitelist whitelist =
+        new Whitelist(Mailbox.STORAGE_ROOT+"/db/whitelist.sqlite");
+
     public static final int MAX_MESSAGE_SIZE =
         Integer.parseInt(System.getProperty("org.ibex.mail.smtp.maxMessageSize", "-1"));
 
index 46c58f3..248700e 100644 (file)
@@ -134,6 +134,7 @@ public class Script extends JS.Obj implements Target {
             case "mail.my.prefs": try {
                     return new org.ibex.js.Directory(new File("/etc/org.ibex.mail.prefs"));
             } catch (IOException e) { throw new JSExn(e.toString()); }
+            case "mail.whitelist": return JSReflection.wrap(org.ibex.mail.protocol.SMTP.whitelist);
             case "mail.my.mailbox":
                 FileBasedMailbox root = FileBasedMailbox.getFileBasedMailbox(Mailbox.STORAGE_ROOT, true);
                 return root.slash("user", true).slash("megacz", true);
index 587cf95..a4830a1 100644 (file)
@@ -72,7 +72,7 @@ public class SqliteJdbcMailbox extends Mailbox.Default {
         public void    delete() { throw new RuntimeException("not supported"); }
     }
 
-    public static String streamToString(Stream stream) throws Exception {
+    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");