package org.ibex.mail; import org.ibex.io.*; import org.ibex.net.*; import org.ibex.mail.protocol.*; import org.ibex.util.*; import org.ibex.net.*; import java.sql.*; import java.net.*; import java.io.*; import java.util.*; import java.sql.Timestamp; import java.sql.Connection; public class Whitelist extends SqliteDB { public Whitelist(String filename) throws SQLException { super(filename); SqliteTable whitelist = getTable("whitelist", "(email)"); whitelist.createIndex("email"); SqliteTable pending = getTable("pending", "(spamid,email,message,date)"); pending.reap("date"); pending.createIndex("spamid"); pending.createIndex("email"); } public boolean handleRequest(org.ibex.net.Connection c) { try { Socket sock = c.getSocket(); 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 Found\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); if (url.endsWith(".txt")) url = url.substring(0, url.length()-4); 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); } return true; } public synchronized boolean isWhitelisted(Address a) { try { if (a==null) return false; 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+"\""); HashSet hsm = new HashSet(); synchronized(this) { do { addWhitelist(Address.parse(rs.getString(1))); Message m = Message.newMessage(new Fountain.StringFountain(rs.getString(2))); Address a = m.headers.get("reply-to")==null ? null : Address.parse(m.headers.get("reply-to")); if (a!=null) addWhitelist(a); a = m.from; if (a!=null) addWhitelist(a); a = m.envelopeFrom; if (a!=null) addWhitelist(a); hsm.add(m); if (m.cc != null) for(Address aa : m.cc) { if (aa!= null) addWhitelist(aa); } } while (rs.next()); } for(Message m : hsm) Target.root.accept(m); } catch (SQLException e) { throw new RuntimeException(e); } } public void challenge(Message m) { try { // FIXME: don't challenge emails with binaries in them; // reject them outright and have the sender send an // initial message w/o a binary. // FIXME: use Auto here!!! // The challenge should refer to the message-id of the mail being challenged. // FIXME: watch outgoing MessageID's: if something comes // back with an In-Reply-To mentioning a MessageID from // the last few days, auto-whitelist them. // FIXME: important that "From" on the challenge matches // RCPT TO on the original message. 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; if (m.envelopeTo==null || m.envelopeTo.equals("null") || m.envelopeTo.equals("")) { Log.warn(this, "message is missing a to/replyto/envelopeto header; cannot accept"); return; } if (m.headers.get("Auto-Submitted") != null && m.headers.get("Auto-Submitted").toLowerCase().indexOf("auto-replied")!=-1) { Log.warn(this, "refusing to send a challenge to a message "+ "with Auto-Submitted=\""+m.headers.get("Auto-Submitted")+"\""); return; } if (m.headers.get("List-Id") != null || m.headers.get("List-Post") != null) { Log.warn(this, "refusing to send a challenge to a message with a List-Id or List-Post header"); return; } 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)+".txt"; 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)); boolean send = false; synchronized(this) { PreparedStatement query = conn.prepareStatement("select email from pending where email=?"); query.setString(1, to.toString(false)); ResultSet rs = query.executeQuery(); if (rs.next()) { Log.warn(this, "already challenged " + to.toString(false) + "; not challenging again."); } else { send = true; } } if (send) if (!SMTP.Outgoing.attempt(challenge)) throw new RuntimeException("attempted to send challenge but could not: " + m.summary()); synchronized(this) { PreparedStatement add = conn.prepareStatement("insert into pending values(?,?,?,?)"); add.setString(1, messageid); add.setString(2, to.toString(false)); add.setString(3, SqliteDB.streamToString(m.getStream())); add.setTimestamp(4, new Timestamp(System.currentTimeMillis())); add.executeUpdate(); } } catch (Exception e) { throw new RuntimeException(e); } } }