From d108f90cc3a2ed1038c31bdcee4e4134eb7ffb66 Mon Sep 17 00:00:00 2001 From: adam Date: Sat, 20 Jan 2007 22:29:33 +0000 Subject: [PATCH] add whitelisting code darcs-hash:20070120222933-5007d-9623d8533ef5f1705be241ed8694d1a96ffb35a7.gz --- src/org/ibex/mail/Whitelist.java | 200 +++++++++++++++++++++++ src/org/ibex/mail/protocol/SMTP.java | 3 + src/org/ibex/mail/target/Script.java | 1 + src/org/ibex/mail/target/SqliteJdbcMailbox.java | 2 +- 4 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 src/org/ibex/mail/Whitelist.java diff --git a/src/org/ibex/mail/Whitelist.java b/src/org/ibex/mail/Whitelist.java new file mode 100644 index 0000000..c8f54ea --- /dev/null +++ b/src/org/ibex/mail/Whitelist.java @@ -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(); + } +} + diff --git a/src/org/ibex/mail/protocol/SMTP.java b/src/org/ibex/mail/protocol/SMTP.java index bca478b..37686c6 100644 --- a/src/org/ibex/mail/protocol/SMTP.java +++ b/src/org/ibex/mail/protocol/SMTP.java @@ -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")); diff --git a/src/org/ibex/mail/target/Script.java b/src/org/ibex/mail/target/Script.java index 46c58f3..248700e 100644 --- a/src/org/ibex/mail/target/Script.java +++ b/src/org/ibex/mail/target/Script.java @@ -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); diff --git a/src/org/ibex/mail/target/SqliteJdbcMailbox.java b/src/org/ibex/mail/target/SqliteJdbcMailbox.java index 587cf95..a4830a1 100644 --- a/src/org/ibex/mail/target/SqliteJdbcMailbox.java +++ b/src/org/ibex/mail/target/SqliteJdbcMailbox.java @@ -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"); -- 1.7.10.4