Switch from Java Mail to Apache Commons NET basic SMTP client

This removes the dependency on the encumbered Java Mail client made
by Sun Microsystems, and replaces it with a very simple SMTP library
under the Apache License.

Configuration for the email client is now set in gerrit.config, by
the same properties that "git send-email" would honor.  Unfortunately
we don't support TLS/SSL as a result of this move.

Signed-off-by: Shawn O. Pearce <sop@google.com>
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index d16218f..60a1ba5 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -369,6 +369,30 @@
 +
 By default, false, as not all instances will deploy repo.
 
+Section sendemail
+~~~~~~~~~~~~~~~~~
+
+sendemail.smtpServer::
++
+Hostname (or IP address) of a SMTP server that will relay
+messages generated by Gerrit to end users.
++
+By default, 127.0.0.1 (aka localhost).
+
+sendemail.smtpServerPort::
++
+Port number of the SMTP server in sendemail.smtpserver.
++
+By default, 25.
+
+sendemail.smtpUser::
++
+User name to authenticate with, if required for relay.
+
+sendemail.smtpPass::
++
+Password for the account named by sendemail.smtpUser.
+
 Section sshd
 ~~~~~~~~~~~~
 
diff --git a/Documentation/install.txt b/Documentation/install.txt
index fc87ecd..b545cde 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -21,7 +21,6 @@
 
 * link:http://sourceforge.net/project/showfiles.php?group_id=25357[c3p0 JDBC Driver]
 * link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
-* link:http://java.sun.com/products/javamail/downloads/index.html[JavaMail]
 
 
 Downloading Gerrit
@@ -236,8 +235,6 @@
 
 * link:http://www.bouncycastle.org/java.html[Bouncy Castle Crypto API]
 
-Jetty comes with JavaMail, so there is no need to install it.
-
 Copy Gerrit into the deployment:
 ====
   java -jar gerrit.war --cat extra/jetty_gerrit.xml >$JETTY_HOME/contexts/gerrit.xml
@@ -333,14 +330,6 @@
 container's extensions directory, but gerrit.war could also be
 manually repacked to include it.
 
-('Optional') Configure the JNDI name `mail/Outgoing` for the web
-application context to be a factory for a `javax.mail.Session`,
-with the connection information necessary to send outgoing emails.
-You may need to download and install the Java Mail JARs in your
-container's classpath.  If this is not configured, Gerrit will
-function, but will not be able to send email.
-
-
 [[apache2]]
 Apache2 Reverse Proxy
 ~~~~~~~~~~~~~~~~~~~~~
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 594348b..4a4b197 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -18,6 +18,7 @@
 Apache Commons Codec        <<apache2,Apache License 2.0>>
 Apache Commons Logging      <<apache2,Apache License 2.0>>
 Apache Commons Http Client  <<apache2,Apache License 2.0>>
+Apache Commons Net          <<apache2,Apache License 2.0>>
 Apache Log4J                <<apache2,Apache License 2.0>>
 Apache MINA                 <<apache2,Apache License 2.0>>
 Apache SSHD                 <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>>
diff --git a/pom.xml b/pom.xml
index 150c5e1..aa9efe8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -536,10 +536,9 @@
     </dependency>
 
     <dependency>
-      <groupId>javax.mail</groupId>
-      <artifactId>mail</artifactId>
-      <version>1.4.1</version>
-      <scope>provided</scope>
+      <groupId>commons-net</groupId>
+      <artifactId>commons-net</artifactId>
+      <version>2.0</version>
     </dependency>
 
     <dependency>
diff --git a/src/main/java/com/google/gerrit/git/MergeOp.java b/src/main/java/com/google/gerrit/git/MergeOp.java
index 9d40429..5dcd27d 100644
--- a/src/main/java/com/google/gerrit/git/MergeOp.java
+++ b/src/main/java/com/google/gerrit/git/MergeOp.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.client.workflow.FunctionState;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritServer;
+import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gwtorm.client.OrmException;
@@ -65,8 +66,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.mail.MessagingException;
-
 /**
  * Merges changes in submission order into a single branch.
  * <p>
@@ -719,7 +718,7 @@
             cm.send();
           } catch (OrmException e) {
             log.error("Cannot submit patch set for Change " + c.getId() + " due to a missing dependency.", e);
-          } catch (MessagingException e) {
+          } catch (EmailException e) {
             log.error("Cannot submit patch set for Change " + c.getId() + " due to a missing dependency.", e);
           }
 
@@ -832,7 +831,7 @@
       cm.send();
     } catch (OrmException e) {
       log.error("Cannot send email for submitted patch set " + c.getId(), e);
-    } catch (MessagingException e) {
+    } catch (EmailException e) {
       log.error("Cannot send email for submitted patch set " + c.getId(), e);
     }
   }
@@ -873,7 +872,7 @@
       cm.send();
     } catch (OrmException e) {
       log.error("Cannot submit patch set for Change " + c.getId() + " due to a path conflict.", e);
-    } catch (MessagingException e) {
+    } catch (EmailException e) {
       log.error("Cannot submit patch set for Change " + c.getId() + " due to a path conflict.", e);
     }
   }
diff --git a/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java b/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java
index a0696de..5768e33 100644
--- a/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java
+++ b/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java
@@ -29,6 +29,8 @@
 import com.google.gerrit.client.rpc.ContactInformationStoreException;
 import com.google.gerrit.client.rpc.InvalidSshKeyException;
 import com.google.gerrit.client.rpc.NoSuchEntityException;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.ssh.SshUtil;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
@@ -49,17 +51,11 @@
 import java.security.spec.InvalidKeySpecException;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
-import javax.mail.Message;
-import javax.mail.MessagingException;
-import javax.mail.Transport;
-import javax.mail.internet.InternetAddress;
-import javax.mail.internet.MimeMessage;
 import javax.servlet.http.HttpServletRequest;
 
 class AccountSecurityImpl extends BaseServiceImplementation implements
@@ -282,71 +278,15 @@
     final PersonIdent gi = server.newGerritPersonIdent();
     final HttpServletRequest req =
         GerritJsonServlet.getCurrentCall().getHttpServletRequest();
-    final StringBuffer url = req.getRequestURL();
-    final StringBuilder m = new StringBuilder();
-
-    url.setLength(url.lastIndexOf("/")); // cut "AccountSecurity"
-    url.setLength(url.lastIndexOf("/")); // cut "rpc"
-    url.setLength(url.lastIndexOf("/")); // cut "gerrit"
-    url.append("/Gerrit#VE,");
-
     try {
-      url.append(server.getEmailRegistrationToken().newToken(
-          Base64.encodeBytes(address.getBytes("UTF-8"))));
-    } catch (XsrfException e) {
-      cb.onFailure(e);
-      return;
-    } catch (UnsupportedEncodingException e) {
-      cb.onFailure(e);
-      return;
-    }
-
-    m.append("Welcome to Gerrit Code Review at ");
-    m.append(req.getServerName());
-    m.append(".\n");
-
-    m.append("\n");
-    m.append("To add a verified email address to your user account, please\n");
-    m.append("click on the following link:\n");
-    m.append("\n");
-    m.append(url);
-    m.append("\n");
-
-    m.append("\n");
-    m.append("If you have received this mail in error,"
-        + " you do not need to take any\n");
-    m.append("action to cancel the account."
-        + " The account will not be activated, and\n");
-    m.append("you will not receive any further emails.\n");
-
-    m.append("\n");
-    m.append("If clicking the link above does not work,"
-        + " copy and paste the URL in a\n");
-    m.append("new browser window instead.\n");
-
-    m.append("\n");
-    m.append("This is a send-only email address."
-        + "  Replies to this message will not\n");
-    m.append("be read or answered.\n");
-
-    final javax.mail.Session out = server.getOutgoingMail();
-    if (out == null) {
-      cb.onFailure(new IllegalStateException("Outgoing mail is disabled"));
-      return;
-    }
-    try {
-      final MimeMessage msg = new MimeMessage(out);
-      msg.setFrom(new InternetAddress(gi.getEmailAddress(), gi.getName()));
-      msg.setRecipients(Message.RecipientType.TO, address);
-      msg.setSubject("[Gerrit Code Review] Email Verification");
-      msg.setSentDate(new Date());
-      msg.setText(m.toString());
-      Transport.send(msg);
+      final RegisterNewEmailSender sender;
+      sender = new RegisterNewEmailSender(server, address, req);
+      sender.send();
       cb.onSuccess(VoidResult.INSTANCE);
-    } catch (MessagingException e) {
+    } catch (EmailException e) {
       log.error("Cannot send email verification message to " + address, e);
       cb.onFailure(e);
-    } catch (UnsupportedEncodingException e) {
+    } catch (RuntimeException e) {
       log.error("Cannot send email verification message to " + address, e);
       cb.onFailure(e);
     }
diff --git a/src/main/java/com/google/gerrit/server/GerritServer.java b/src/main/java/com/google/gerrit/server/GerritServer.java
index 6846c77..04d137a 100644
--- a/src/main/java/com/google/gerrit/server/GerritServer.java
+++ b/src/main/java/com/google/gerrit/server/GerritServer.java
@@ -40,6 +40,7 @@
 import com.google.gerrit.git.PushQueue;
 import com.google.gerrit.git.RepositoryCache;
 import com.google.gerrit.git.WorkQueue;
+import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.patch.DiffCacheEntryFactory;
 import com.google.gerrit.server.ssh.SshKeyCacheEntryFactory;
 import com.google.gwtjsonrpc.server.SignedToken;
@@ -59,6 +60,9 @@
 import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
 
 import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.net.smtp.AuthSMTPClient;
+import org.apache.commons.net.smtp.SMTPClient;
+import org.apache.commons.net.smtp.SMTPReply;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.spearce.jgit.lib.PersonIdent;
@@ -185,7 +189,6 @@
   private final SignedToken account;
   private final SignedToken emailReg;
   private final RepositoryCache repositories;
-  private final javax.mail.Session outgoingMail;
   private final SelfPopulatingCache diffCache;
   private final SelfPopulatingCache sshKeysCache;
 
@@ -235,8 +238,6 @@
       repositories = null;
     }
 
-    outgoingMail = createOutgoingMail();
-
     final ReviewDb c = db.open();
     try {
       loadGerritConfig(c);
@@ -692,13 +693,49 @@
     Common.setGerritConfig(r);
   }
 
-  private javax.mail.Session createOutgoingMail() {
-    final String dsName = "java:comp/env/mail/Outgoing";
-    try {
-      return (javax.mail.Session) new InitialContext().lookup(dsName);
-    } catch (NamingException namingErr) {
-      return null;
+  public SMTPClient createOutgoingMail() throws EmailException {
+    final RepositoryConfig cfg = getGerritConfig();
+    String smtpHost = cfg.getString("sendemail", null, "smtpserver");
+    if (smtpHost == null) {
+      smtpHost = "127.0.0.1";
     }
+    int smtpPort = cfg.getInt("sendemail", null, "smtpserverport", 25);
+
+    String smtpUser = cfg.getString("sendemail", null, "smtpuser");
+    String smtpPass = cfg.getString("sendemail", null, "smtpuserpass");
+
+    final AuthSMTPClient client = new AuthSMTPClient("UTF-8");
+    try {
+      client.connect(smtpHost, smtpPort);
+      if (!SMTPReply.isPositiveCompletion(client.getReplyCode())) {
+        throw new EmailException("SMTP server rejected connection");
+      }
+      if (!client.login()) {
+        String e = client.getReplyString();
+        throw new EmailException("SMTP server rejected login: " + e);
+      }
+      if (smtpUser != null && !client.auth(smtpUser, smtpPass)) {
+        String e = client.getReplyString();
+        throw new EmailException("SMTP server rejected auth: " + e);
+      }
+    } catch (IOException e) {
+      if (client.isConnected()) {
+        try {
+          client.disconnect();
+        } catch (IOException e2) {
+        }
+      }
+      throw new EmailException(e.getMessage(), e);
+    } catch (EmailException e) {
+      if (client.isConnected()) {
+        try {
+          client.disconnect();
+        } catch (IOException e2) {
+        }
+      }
+      throw e;
+    }
+    return client;
   }
 
   private void reconfigureWindowCache() {
@@ -849,11 +886,6 @@
     return sshKeysCache;
   }
 
-  /** The mail session used to send messages; null if not configured. */
-  public javax.mail.Session getOutgoingMail() {
-    return outgoingMail;
-  }
-
   /** Get a new identity representing this Gerrit server in Git. */
   public PersonIdent newGerritPersonIdent() {
     String name = getGerritConfig().getString("user", null, "name");
diff --git a/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 64b055c..d4b15f9 100644
--- a/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.server.GerritServer;
 
-import javax.mail.MessagingException;
-
 /** Send notice about a change being abandoned by its owner. */
 public class AbandonedSender extends ReplyToChangeSender {
   public AbandonedSender(GerritServer gs, Change c) {
@@ -26,7 +24,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     ccAllApprovals();
diff --git a/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index 33f5962..106bae1 100644
--- a/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.server.GerritServer;
 
-import javax.mail.MessagingException;
-
 /** Asks a user to review a change. */
 public class AddReviewerSender extends NewChangeSender {
   public AddReviewerSender(GerritServer gs, Change c) {
@@ -26,7 +24,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     ccExistingReviewers();
diff --git a/src/main/java/com/google/gerrit/server/mail/Address.java b/src/main/java/com/google/gerrit/server/mail/Address.java
new file mode 100644
index 0000000..9668188
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+class Address {
+  String name;
+  String email;
+
+  Address(String email) {
+    this(null, email);
+  }
+
+  Address(String name, String email) {
+    this.name = name;
+    this.email = email;
+  }
+
+  @Override
+  public String toString() {
+    return toHeaderString();
+  }
+
+  String toHeaderString() {
+    if (name != null) {
+      return quotedPhrase(name) + " <" + email + ">";
+    } else if (isSimple()) {
+      return email;
+    }
+    return "<" + email + ">";
+  }
+
+  private static final String MUST_QUOTE_EMAIL = "()<>,;:\\\"[]";
+  private static final String MUST_QUOTE_NAME = MUST_QUOTE_EMAIL + "@.";
+
+  private boolean isSimple() {
+    for (int i = 0; i < email.length(); i++) {
+      final char c = email.charAt(i);
+      if (c <= ' ' || 0x7F <= c || MUST_QUOTE_EMAIL.indexOf(c) != -1) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private static String quotedPhrase(final String name) {
+    for (int i = 0; i < name.length(); i++) {
+      final char c = name.charAt(i);
+      if (c < ' ' || 0x7F <= c || MUST_QUOTE_NAME.indexOf(c) != -1) {
+        return wrapInQuotes(name);
+      }
+    }
+    return name;
+  }
+
+  private static String wrapInQuotes(final String name) {
+    final StringBuilder r = new StringBuilder(2 + name.length());
+    r.append('"');
+    for (int i = 0; i < name.length(); i++) {
+      char c = name.charAt(i);
+      if (c == '"' || c == '\\') {
+        r.append('\\');
+      }
+      r.append(c);
+    }
+    r.append('"');
+    return r.toString();
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index 64699bb..fe06969 100644
--- a/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -29,8 +29,6 @@
 import java.util.List;
 import java.util.Map;
 
-import javax.mail.MessagingException;
-
 /** Send comments, after the author of them hit used Publish Comments in the UI. */
 public class CommentSender extends ReplyToChangeSender {
   private List<PatchLineComment> inlineComments = Collections.emptyList();
@@ -44,7 +42,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     ccAllApprovals();
diff --git a/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index 95c1b49..5865f49 100644
--- a/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -25,9 +25,6 @@
 import java.util.HashSet;
 import java.util.Set;
 
-import javax.mail.MessagingException;
-import javax.mail.Message.RecipientType;
-
 /** Notify interested parties of a brand new change. */
 public class CreateChangeSender extends NewChangeSender {
   public CreateChangeSender(GerritServer gs, Change c) {
@@ -35,13 +32,13 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     bccWatchers();
   }
 
-  private void bccWatchers() throws MessagingException {
+  private void bccWatchers() {
     if (db != null) {
       try {
         // BCC anyone else who has interest in this project's changes
diff --git a/src/main/java/com/google/gerrit/server/mail/EmailException.java b/src/main/java/com/google/gerrit/server/mail/EmailException.java
new file mode 100644
index 0000000..7fe817a
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/EmailException.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+public class EmailException extends Exception {
+  public EmailException(String msg) {
+    super(msg);
+  }
+
+  public EmailException(String msg, Throwable why) {
+    super(msg, why);
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
new file mode 100644
index 0000000..151e560
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+abstract class EmailHeader {
+  abstract boolean isEmpty();
+
+  abstract void write(Writer w) throws IOException;
+
+  static class String extends EmailHeader {
+    private java.lang.String value;
+
+    String(java.lang.String v) {
+      value = v;
+    }
+
+    @Override
+    boolean isEmpty() {
+      return value == null || value.length() == 0;
+    }
+
+    @Override
+    void write(Writer w) throws IOException {
+      w.write(value);
+    }
+  }
+
+  static class Date extends EmailHeader {
+    private java.util.Date value;
+
+    Date(java.util.Date v) {
+      value = v;
+    }
+
+    @Override
+    boolean isEmpty() {
+      return value == null;
+    }
+
+    @Override
+    void write(Writer w) throws IOException {
+      final SimpleDateFormat fmt;
+      // Mon, 1 Jun 2009 10:49:44 -0700
+      fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
+      w.write(fmt.format(value));
+    }
+  }
+
+  static class AddressList extends EmailHeader {
+    private final List<Address> list = new ArrayList<Address>();
+
+    AddressList() {
+    }
+
+    AddressList(Address addr) {
+      add(addr);
+    }
+
+    void add(Address addr) {
+      list.add(addr);
+    }
+
+    @Override
+    boolean isEmpty() {
+      return list.isEmpty();
+    }
+
+    @Override
+    void write(Writer w) throws IOException {
+      int len = 8;
+      boolean first = true;
+      for (final Address addr : list) {
+        java.lang.String s = addr.toHeaderString();
+        if (!first && 72 < len + s.length()) {
+          w.write(",\r\n\t");
+          len = 8;
+          first = true;
+        }
+        if (!first) {
+          w.write(", ");
+          first = false;
+        }
+        w.write(s);
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
index 6173418..5f5f4de 100644
--- a/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.server.GerritServer;
 
-import javax.mail.MessagingException;
-
 /** Send notice about a change failing to merged. */
 public class MergeFailSender extends ReplyToChangeSender {
   public MergeFailSender(GerritServer gs, Change c) {
@@ -26,7 +24,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     ccExistingReviewers();
diff --git a/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 92634c4..ec63776 100644
--- a/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -29,9 +29,6 @@
 import java.util.HashMap;
 import java.util.Map;
 
-import javax.mail.MessagingException;
-import javax.mail.Message.RecipientType;
-
 /** Send notice about a change successfully merged. */
 public class MergedSender extends ReplyToChangeSender {
   public MergedSender(GerritServer gs, Change c) {
@@ -39,7 +36,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     ccAllApprovals();
@@ -141,7 +138,7 @@
     m.put(ca.getCategoryId(), ca);
   }
 
-  private void bccWatchesNotifySubmittedChanges() throws MessagingException {
+  private void bccWatchesNotifySubmittedChanges() {
     if (db != null) {
       try {
         // BCC anyone else who has interest in this project's changes
diff --git a/src/main/java/com/google/gerrit/server/mail/MimeMessage.java b/src/main/java/com/google/gerrit/server/mail/MimeMessage.java
deleted file mode 100644
index 7f28dca..0000000
--- a/src/main/java/com/google/gerrit/server/mail/MimeMessage.java
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.mail;
-
-import javax.mail.MessagingException;
-import javax.mail.Session;
-
-class MimeMessage extends javax.mail.internet.MimeMessage {
-  static final String MESSAGE_ID = "Message-ID";
-
-  MimeMessage(final Session session) {
-    super(session);
-  }
-
-  private String messageID;
-
-  void setMessageID(final String id) {
-    messageID = id;
-  }
-
-  @Override
-  public void setHeader(String name, String value) throws MessagingException {
-    if (MESSAGE_ID.equalsIgnoreCase(name)) {
-      messageID = value;
-    } else {
-      super.setHeader(name, value);
-    }
-  }
-
-  @Override
-  protected void updateMessageID() throws MessagingException {
-    if (messageID != null) {
-      super.setHeader(MESSAGE_ID, messageID);
-    } else {
-      super.updateMessageID();
-    }
-  }
-}
diff --git a/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 2d27cad..4329ad2 100644
--- a/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -23,9 +23,6 @@
 import java.util.Iterator;
 import java.util.Set;
 
-import javax.mail.MessagingException;
-import javax.mail.Message.RecipientType;
-
 /** Sends an email alerting a user to a new change for them to review. */
 public abstract class NewChangeSender extends OutgoingEmail {
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
@@ -44,10 +41,10 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
-    setHeader(MimeMessage.MESSAGE_ID, getChangeMessageThreadId());
+    setHeader("Message-ID", getChangeMessageThreadId());
 
     add(RecipientType.TO, reviewers);
     add(RecipientType.CC, extraCC);
diff --git a/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index 2c96f1c..042ead3 100644
--- a/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -32,34 +32,41 @@
 import com.google.gerrit.server.GerritServer;
 import com.google.gwtorm.client.OrmException;
 
+import org.apache.commons.net.smtp.SMTPClient;
 import org.spearce.jgit.lib.PersonIdent;
 
-import java.io.UnsupportedEncodingException;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.Writer;
 import java.net.InetAddress;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.UnknownHostException;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Random;
 
-import javax.mail.Address;
-import javax.mail.MessagingException;
-import javax.mail.Transport;
-import javax.mail.Message.RecipientType;
-import javax.mail.internet.AddressException;
-import javax.mail.internet.InternetAddress;
 import javax.servlet.http.HttpServletRequest;
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
+  private static final String HDR_TO = "To";
+  private static final String HDR_CC = "CC";
+
+  private static final Random RNG = new Random();
   private final String messageClass;
   protected final GerritServer server;
-  private final javax.mail.Session transport;
   protected final Change change;
   protected final String projectName;
   private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
-  private MimeMessage msg;
+  private final Map<String, EmailHeader> headers;
+  private final List<String> smtpRcptTo = new ArrayList<String>();
+  private Address smtpFromAddress;
   private StringBuilder body;
   private boolean inFooter;
 
@@ -72,10 +79,10 @@
 
   protected OutgoingEmail(final GerritServer gs, final Change c, final String mc) {
     server = gs;
-    transport = server.getOutgoingMail();
     change = c;
-    projectName = change.getDest().getParentKey().get();
+    projectName = change != null ? change.getDest().getParentKey().get() : null;
     messageClass = mc;
+    headers = new LinkedHashMap<String, EmailHeader>();
   }
 
   public void setFrom(final Account.Id id) {
@@ -99,8 +106,12 @@
     db = d;
   }
 
-  /** Format and enqueue the message for delivery. */
-  public void send() throws MessagingException {
+  /**
+   * Format and enqueue the message for delivery.
+   * 
+   * @throws EmailException
+   */
+  public void send() throws EmailException {
     init();
     format();
     if (shouldSendMessage()) {
@@ -111,30 +122,89 @@
         //
         add(RecipientType.CC, fromId);
       }
-      if (getChangeUrl() != null) {
-        openFooter();
-        appendText("To view visit ");
-        appendText(getChangeUrl());
-        appendText("\n");
-      }
-      if (getSettingsUrl() != null) {
-        openFooter();
-        appendText("To unsubscribe, visit ");
-        appendText(getSettingsUrl());
-        appendText("\n");
+      if (change != null) {
+        if (getChangeUrl() != null) {
+          openFooter();
+          appendText("To view visit ");
+          appendText(getChangeUrl());
+          appendText("\n");
+        }
+        if (getSettingsUrl() != null) {
+          openFooter();
+          appendText("To unsubscribe, visit ");
+          appendText(getSettingsUrl());
+          appendText("\n");
+        }
+
+        if (inFooter) {
+          appendText("\n");
+        } else {
+          openFooter();
+        }
+        appendText("Gerrit-MessageType: " + messageClass + "\n");
+        appendText("Gerrit-Project: " + projectName + "\n");
+        appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n");
       }
 
-      if (inFooter) {
-        appendText("\n");
-      } else {
-        openFooter();
-      }
-      appendText("Gerrit-MessageType: " + messageClass + "\n");
-      appendText("Gerrit-Project: " + projectName + "\n");
-      appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n");
+      try {
+        final SMTPClient client = server.createOutgoingMail();
+        try {
+          if (!client.setSender(smtpFromAddress.email)) {
+            throw new EmailException("SMTP server rejected from "
+                + smtpFromAddress);
+          }
 
-      msg.setText(body.toString(), "UTF-8");
-      Transport.send(msg);
+          for (String emailAddress : smtpRcptTo) {
+            if (!client.addRecipient(emailAddress)) {
+              String error = client.getReplyString();
+              throw new EmailException("SMTP server rejected rcpt "
+                  + emailAddress + ": " + error);
+            }
+          }
+
+          if (headers.get("Message-ID").isEmpty()) {
+            final StringBuilder rndid = new StringBuilder();
+            rndid.append("<");
+            rndid.append(System.currentTimeMillis());
+            rndid.append("-");
+            rndid.append(Integer.toString(RNG.nextInt(999999), 36));
+            rndid.append("@");
+            rndid.append(InetAddress.getLocalHost().getCanonicalHostName());
+            rndid.append(">");
+            setHeader("Message-ID", rndid.toString());
+          }
+
+          Writer w = client.sendMessageData();
+          if (w == null) {
+            throw new EmailException("SMTP server rejected message body");
+          }
+          w = new BufferedWriter(w);
+
+          for (Map.Entry<String, EmailHeader> h : headers.entrySet()) {
+            if (!h.getValue().isEmpty()) {
+              w.write(h.getKey());
+              w.write(": ");
+              h.getValue().write(w);
+              w.write("\r\n");
+            }
+          }
+
+          w.write("\r\n");
+          w.write(body.toString());
+          w.flush();
+          w.close();
+
+          if (!client.completePendingCommand()) {
+            throw new EmailException("SMTP server rejected message body");
+          }
+
+          client.logout();
+        } finally {
+          client.disconnect();
+        }
+      } catch (IOException e) {
+        throw new EmailException("Cannot send outgoing email", e);
+      }
     }
   }
 
@@ -142,25 +212,35 @@
   protected abstract void format();
 
   /** Setup the message headers and envelope (TO, CC, BCC). */
-  protected void init() throws MessagingException {
-    msg = new MimeMessage(transport);
+  protected void init() {
+    smtpFromAddress = computeFrom();
     if (changeMessage != null && changeMessage.getWrittenOn() != null) {
-      msg.setSentDate(new Date(changeMessage.getWrittenOn().getTime()));
+      setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
     } else {
-      msg.setSentDate(new Date());
+      setHeader("Date", new Date());
     }
-    msg.setFrom(computeFrom());
+    headers.put("From", new EmailHeader.AddressList(smtpFromAddress));
+    headers.put(HDR_TO, new EmailHeader.AddressList());
+    headers.put(HDR_CC, new EmailHeader.AddressList());
+    if (change != null) {
+      setChangeSubjectHeader();
+    }
+    setHeader("Message-ID", "");
+    setHeader("MIME-Version", "1.0");
+    setHeader("Content-Type", "text/plain; charset=UTF-8");
+    setHeader("Content-Disposition", "inline");
     setHeader("User-Agent", "Gerrit/" + Version.getVersion());
-    setHeader("X-Gerrit-ChangeId", "" + change.getChangeId());
     setHeader("X-Gerrit-MessageType", messageClass);
-    setListIdHeader();
-    setChangeUrlHeader();
-    setCommitIdHeader();
-    setSubjectHeader();
+    if (change != null) {
+      setHeader("X-Gerrit-ChangeId", "" + change.getChangeId());
+      setListIdHeader();
+      setChangeUrlHeader();
+      setCommitIdHeader();
+    }
     body = new StringBuilder();
     inFooter = false;
 
-    if (db != null) {
+    if (change != null && db != null) {
       if (patchSet == null) {
         try {
           patchSet = db.patchSets().get(change.currentPatchSetId());
@@ -179,19 +259,16 @@
     }
   }
 
-  private Address computeFrom() throws AddressException {
+  private Address computeFrom() {
     if (fromId != null) {
       return toAddress(fromId);
     }
+
     final PersonIdent pi = server.newGerritPersonIdent();
-    try {
-      return new InternetAddress(pi.getName(), pi.getEmailAddress());
-    } catch (UnsupportedEncodingException e) {
-      return new InternetAddress(pi.getEmailAddress());
-    }
+    return new Address(pi.getName(), pi.getEmailAddress());
   }
 
-  private void setListIdHeader() throws MessagingException {
+  private void setListIdHeader() {
     // Set a reasonable list id so that filters can be used to sort messages
     //
     final StringBuilder listid = new StringBuilder();
@@ -208,14 +285,14 @@
     }
   }
 
-  private void setChangeUrlHeader() throws MessagingException {
+  private void setChangeUrlHeader() {
     final String u = getChangeUrl();
     if (u != null) {
       setHeader("X-Gerrit-ChangeURL", "<" + u + ">");
     }
   }
 
-  private void setCommitIdHeader() throws MessagingException {
+  private void setCommitIdHeader() {
     if (patchSet != null && patchSet.getRevision() != null
         && patchSet.getRevision().get() != null
         && patchSet.getRevision().get().length() > 0) {
@@ -223,7 +300,7 @@
     }
   }
 
-  private void setSubjectHeader() throws MessagingException {
+  private void setChangeSubjectHeader() {
     final StringBuilder subj = new StringBuilder();
     subj.append("[");
     subj.append(change.getDest().getShortName());
@@ -239,7 +316,7 @@
     } else {
       subj.append(change.getSubject());
     }
-    msg.setSubject(subj.toString());
+    setHeader("Subject", subj.toString());
   }
 
   private String getGerritHost() {
@@ -272,7 +349,7 @@
 
   /** Get a link to the change; null if the server doesn't know its own address. */
   protected String getChangeUrl() {
-    if (getGerritUrl() != null) {
+    if (change != null && getGerritUrl() != null) {
       final StringBuilder r = new StringBuilder();
       r.append(getGerritUrl());
       r.append(change.getChangeId());
@@ -313,9 +390,12 @@
   }
 
   /** Set a header in the outgoing message. */
-  protected void setHeader(final String name, final String value)
-      throws MessagingException {
-    msg.setHeader(name, value);
+  protected void setHeader(final String name, final String value) {
+    headers.put(name, new EmailHeader.String(value));
+  }
+
+  protected void setHeader(final String name, final Date date) {
+    headers.put(name, new EmailHeader.Date(date));
   }
 
   /** Append text to the outgoing email body. */
@@ -397,7 +477,7 @@
     return name;
   }
 
-  private boolean shouldSendMessage() {
+  protected boolean shouldSendMessage() {
     if (body.length() == 0) {
       // If we have no message body, don't send.
       //
@@ -430,16 +510,14 @@
   }
 
   /** Schedule this message for delivery to the listed accounts. */
-  protected void add(final RecipientType rt, final Collection<Account.Id> list)
-      throws MessagingException {
+  protected void add(final RecipientType rt, final Collection<Account.Id> list) {
     for (final Account.Id id : list) {
       add(rt, id);
     }
   }
 
   /** TO or CC all vested parties (change owner, patch set uploader, author). */
-  protected void rcptToAuthors(final RecipientType rt)
-      throws MessagingException {
+  protected void rcptToAuthors(final RecipientType rt) {
     add(rt, change.getOwner());
     if (patchSet != null) {
       add(rt, patchSet.getUploader());
@@ -450,15 +528,14 @@
     }
   }
 
-  private void add(final RecipientType rt, final UserIdentity who)
-      throws MessagingException {
+  private void add(final RecipientType rt, final UserIdentity who) {
     if (who != null && who.getAccount() != null) {
       add(rt, who.getAccount());
     }
   }
 
   /** BCC any user who has starred this change. */
-  protected void bccStarredBy() throws MessagingException {
+  protected void bccStarredBy() {
     if (db != null) {
       try {
         // BCC anyone who has starred this change.
@@ -475,7 +552,7 @@
   }
 
   /** BCC any user who has set "notify all comments" on this project. */
-  protected void bccWatchesNotifyAllComments() throws MessagingException {
+  protected void bccWatchesNotifyAllComments() {
     if (db != null) {
       try {
         // BCC anyone else who has interest in this project's changes
@@ -496,16 +573,16 @@
   }
 
   /** Any user who has published comments on this change. */
-  protected void ccAllApprovals() throws MessagingException {
+  protected void ccAllApprovals() {
     ccApprovals(true);
   }
 
   /** Users who have non-zero approval codes on the change. */
-  protected void ccExistingReviewers() throws MessagingException {
+  protected void ccExistingReviewers() {
     ccApprovals(false);
   }
 
-  private void ccApprovals(final boolean includeZero) throws MessagingException {
+  private void ccApprovals(final boolean includeZero) {
     if (db != null) {
       try {
         // CC anyone else who has posted an approval mark on this change
@@ -522,17 +599,28 @@
   }
 
   /** Schedule delivery of this message to the given account. */
-  protected void add(final RecipientType rt, final Account.Id to)
-      throws MessagingException {
+  protected void add(final RecipientType rt, final Account.Id to) {
     if (rcptTo.add(to)) {
-      final Address addr = toAddress(to);
-      if (addr != null) {
-        msg.addRecipient(rt, addr);
+      add(rt, toAddress(to));
+    }
+  }
+
+  /** Schedule delivery of this message to the given account. */
+  protected void add(final RecipientType rt, final Address addr) {
+    if (addr != null && addr.email != null && addr.email.length() > 0) {
+      smtpRcptTo.add(addr.email);
+      switch (rt) {
+        case TO:
+          ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+          break;
+        case CC:
+          ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+          break;
       }
     }
   }
 
-  private Address toAddress(final Account.Id id) throws AddressException {
+  private Address toAddress(final Account.Id id) {
     final Account a = Common.getAccountCache().get(id);
     if (a == null) {
       return null;
@@ -543,11 +631,6 @@
       return null;
     }
 
-    try {
-      final String an = a.getFullName();
-      return new InternetAddress(e, an);
-    } catch (UnsupportedEncodingException e1) {
-      return new InternetAddress(e);
-    }
+    return new Address(a.getFullName(), e);
   }
 }
diff --git a/src/main/java/com/google/gerrit/server/mail/RecipientType.java b/src/main/java/com/google/gerrit/server/mail/RecipientType.java
new file mode 100644
index 0000000..ea0def0
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/RecipientType.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+public enum RecipientType {
+  TO, CC, BCC
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
new file mode 100644
index 0000000..9e2e235
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.server.GerritServer;
+import com.google.gwtjsonrpc.server.XsrfException;
+
+import org.spearce.jgit.util.Base64;
+
+import java.io.UnsupportedEncodingException;
+
+import javax.servlet.http.HttpServletRequest;
+
+public class RegisterNewEmailSender extends OutgoingEmail {
+  private final HttpServletRequest req;
+  private final String addr;
+
+  public RegisterNewEmailSender(final GerritServer srv, final String address,
+      final HttpServletRequest request) {
+    super(srv, null, "registernewemail");
+    addr = address;
+    req = request;
+  }
+
+  @Override
+  protected void init() {
+    super.init();
+    setHeader("Subject", "[Gerrit Code Review] Email Verification");
+    add(RecipientType.TO, new Address(addr));
+  }
+
+  @Override
+  protected boolean shouldSendMessage() {
+    return true;
+  }
+
+  @Override
+  protected void format() {
+    final StringBuffer url = req.getRequestURL();
+    url.setLength(url.lastIndexOf("/")); // cut "AccountSecurity"
+    url.setLength(url.lastIndexOf("/")); // cut "rpc"
+    url.setLength(url.lastIndexOf("/")); // cut "gerrit"
+    url.append("/Gerrit#VE,");
+    try {
+      url.append(server.getEmailRegistrationToken().newToken(
+          Base64.encodeBytes(addr.getBytes("UTF-8"))));
+    } catch (XsrfException e) {
+      throw new IllegalArgumentException(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalArgumentException(e);
+    }
+
+    appendText("Welcome to Gerrit Code Review at ");
+    appendText(req.getServerName());
+    appendText(".\n");
+
+    appendText("\n");
+    appendText("To add a verified email address to your user account, please\n");
+    appendText("click on the following link:\n");
+    appendText("\n");
+    appendText(url.toString());
+    appendText("\n");
+
+    appendText("\n");
+    appendText("If you have received this mail in error,"
+        + " you do not need to take any\n");
+    appendText("action to cancel the account."
+        + " The account will not be activated, and\n");
+    appendText("you will not receive any further emails.\n");
+
+    appendText("\n");
+    appendText("If clicking the link above does not work,"
+        + " copy and paste the URL in a\n");
+    appendText("new browser window instead.\n");
+
+    appendText("\n");
+    appendText("This is a send-only email address."
+        + "  Replies to this message will not\n");
+    appendText("be read or answered.\n");
+  }
+}
diff --git a/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index da20e54..fab9efc 100644
--- a/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -23,9 +23,6 @@
 import java.util.Iterator;
 import java.util.Set;
 
-import javax.mail.MessagingException;
-import javax.mail.Message.RecipientType;
-
 /** Send notice of new patch sets for reviewers. */
 public class ReplacePatchSetSender extends ReplyToChangeSender {
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
@@ -44,7 +41,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     if (fromId != null) {
diff --git a/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 78d0433..eded131 100644
--- a/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -17,9 +17,6 @@
 import com.google.gerrit.client.reviewdb.Change;
 import com.google.gerrit.server.GerritServer;
 
-import javax.mail.MessagingException;
-import javax.mail.Message.RecipientType;
-
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends OutgoingEmail {
   protected ReplyToChangeSender(GerritServer gs, Change c, String mc) {
@@ -27,7 +24,7 @@
   }
 
   @Override
-  protected void init() throws MessagingException {
+  protected void init() {
     super.init();
 
     final String threadId = getChangeMessageThreadId();
diff --git a/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java b/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java
index ed3ac04..f86ed01 100644
--- a/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java
+++ b/src/main/java/com/google/gerrit/server/patch/PatchDetailServiceImpl.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CommentSender;
+import com.google.gerrit.server.mail.EmailException;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtorm.client.OrmException;
@@ -58,8 +59,6 @@
 import java.util.Map;
 import java.util.Set;
 
-import javax.mail.MessagingException;
-
 public class PatchDetailServiceImpl extends BaseServiceImplementation implements
     PatchDetailService {
   private final Logger log = LoggerFactory.getLogger(getClass());
@@ -175,7 +174,7 @@
           cm.setHttpServletRequest(GerritJsonServlet.getCurrentCall()
               .getHttpServletRequest());
           cm.send();
-        } catch (MessagingException e) {
+        } catch (EmailException e) {
           log.error("Cannot send comments by email for patch set " + psid, e);
           throw new Failure(e);
         }
@@ -333,7 +332,7 @@
           cm.setHttpServletRequest(GerritJsonServlet.getCurrentCall()
               .getHttpServletRequest());
           cm.send();
-        } catch (MessagingException e) {
+        } catch (EmailException e) {
           log.error("Cannot send review request by email for change " + id, e);
           throw new Failure(e);
         }
@@ -415,7 +414,7 @@
             cm.setHttpServletRequest(GerritJsonServlet.getCurrentCall()
                 .getHttpServletRequest());
             cm.send();
-          } catch (MessagingException e) {
+          } catch (EmailException e) {
             log.error("Cannot send abandon change email for change "
                 + change.getChangeId(), e);
             throw new Failure(e);
diff --git a/src/main/java/com/google/gerrit/server/ssh/Receive.java b/src/main/java/com/google/gerrit/server/ssh/Receive.java
index f2df0dd..817d220 100644
--- a/src/main/java/com/google/gerrit/server/ssh/Receive.java
+++ b/src/main/java/com/google/gerrit/server/ssh/Receive.java
@@ -42,6 +42,7 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritServer;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.OrmRunnable;
@@ -83,8 +84,6 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import javax.mail.MessagingException;
-
 /** Receives change upload over SSH using the Git receive-pack protocol. */
 class Receive extends AbstractGitCommand {
   private static final Logger log = LoggerFactory.getLogger(Receive.class);
@@ -714,7 +713,7 @@
       cm.addReviewers(reviewerId);
       cm.addExtraCC(ccId);
       cm.send();
-    } catch (MessagingException e) {
+    } catch (EmailException e) {
       log.error("Cannot send email for new change " + change.getId(), e);
     }
   }
@@ -932,7 +931,7 @@
         cm.addReviewers(oldReviewers);
         cm.addExtraCC(oldCC);
         cm.send();
-      } catch (MessagingException e) {
+      } catch (EmailException e) {
         log.error("Cannot send email for new patch set " + ps.getId(), e);
       }
     }
diff --git a/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java b/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
new file mode 100644
index 0000000..3237287
--- /dev/null
+++ b/src/main/java/org/apache/commons/net/smtp/AuthSMTPClient.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package org.apache.commons.net.smtp;
+
+import org.spearce.jgit.util.Base64;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AuthSMTPClient extends SMTPClient {
+  private String authTypes;
+
+  public AuthSMTPClient(final String charset) {
+    super(charset);
+  }
+
+  @Override
+  public String[] getReplyStrings() {
+    return _replyLines.toArray(new String[_replyLines.size()]);
+  }
+
+  @Override
+  public boolean login() throws IOException {
+    final String name = getLocalAddress().getHostName();
+    if (name == null) {
+      return false;
+    }
+
+    boolean ok = SMTPReply.isPositiveCompletion(sendCommand("EHLO", name));
+    authTypes = "";
+    for (String line : getReplyStrings()) {
+      if (line != null && line.startsWith("250 AUTH ")) {
+        authTypes = line;
+        break;
+      }
+    }
+
+    return ok;
+  }
+
+  public boolean auth(String smtpUser, String smtpPass) throws IOException {
+    List<String> types = Arrays.asList(authTypes.split(" "));
+    if (types.isEmpty()) {
+      // Server didn't advertise authentication support.
+      //
+      return true;
+    }
+
+    if (smtpPass == null) {
+      smtpPass = "";
+    }
+    if (types.contains("CRAM-SHA1")) {
+      return authCram(smtpUser, smtpPass, "SHA1");
+    }
+    if (types.contains("CRAM-MD5")) {
+      return authCram(smtpUser, smtpPass, "MD5");
+    }
+    if (types.contains("PLAIN")) {
+      return authPlain(smtpUser, smtpPass);
+    }
+
+    throw new IOException("Unsupported AUTH: " + authTypes);
+  }
+
+  private boolean authCram(String smtpUser, String smtpPass, String alg)
+      throws UnsupportedEncodingException, IOException {
+    final String macName = "Hmac" + alg;
+    if (sendCommand("AUTH", "CRAM-" + alg) != 334) {
+      return false;
+    }
+
+    final byte[] nonce = Base64.decode(getReplyStrings()[0].split(" ", 2)[1]);
+    final String sec;
+    try {
+      Mac mac = Mac.getInstance(macName);
+      mac.init(new SecretKeySpec(smtpPass.getBytes("UTF-8"), macName));
+      sec = toHex(mac.doFinal(nonce));
+    } catch (NoSuchAlgorithmException e) {
+      throw new IOException("Cannot use CRAM-" + alg, e);
+    } catch (InvalidKeyException e) {
+      throw new IOException("Cannot use CRAM-" + alg, e);
+    }
+
+    String token = smtpUser + ' ' + sec;
+    String cmd = Base64.encodeBytes(token.getBytes("UTF-8"));
+    return SMTPReply.isPositiveCompletion(sendCommand(cmd));
+  }
+
+  private static final char[] hexchar =
+      {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd',
+          'e', 'f'};
+
+  private String toHex(final byte[] b) {
+    final StringBuilder sec = new StringBuilder();
+    for (int i = 0; i < b.length; i++) {
+      final int u = (b[i] >> 4) & 0xf;
+      final int l = b[i] & 0xf;
+      sec.append(hexchar[u]);
+      sec.append(hexchar[l]);
+    }
+    return sec.toString();
+  }
+
+  private boolean authPlain(String smtpUser, String smtpPass)
+      throws UnsupportedEncodingException, IOException {
+    String token = '\0' + smtpUser + '\0' + smtpPass;
+    String cmd = "PLAIN " + Base64.encodeBytes(token.getBytes("UTF-8"));
+    return SMTPReply.isPositiveCompletion(sendCommand("AUTH", cmd));
+  }
+}
diff --git a/src/main/webapp/WEB-INF/extra/jetty_gerrit.xml b/src/main/webapp/WEB-INF/extra/jetty_gerrit.xml
index a3af9df..30b4b18 100644
--- a/src/main/webapp/WEB-INF/extra/jetty_gerrit.xml
+++ b/src/main/webapp/WEB-INF/extra/jetty_gerrit.xml
@@ -55,23 +55,4 @@
       </New>
     </Arg>
   </New>
-
-  <New id="OutgoingSmtp" class="org.mortbay.jetty.plus.naming.Resource">
-    <Arg></Arg>
-    <Arg>mail/Outgoing</Arg>
-    <Arg>
-      <New class="org.mortbay.naming.factories.MailSessionReference">
-        <Set name="properties">
-          <New class="java.util.Properties">
-            <Put name="mail.smtp.host">localhost</Put>
-            <Put name="mail.smtp.port">25</Put>
-          </New>
-        </Set>
-        <!-- Uncomment if you need to authenticate to your SMTP server.
-        <Set name="user">gerrit2</Set>
-        <Set name="password">secretkey</Set>
-             /Uncomment -->
-      </New>
-    </Arg>
-  </New>
 </Configure>
diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml
index 943ceda..0b6fdbd 100644
--- a/src/main/webapp/WEB-INF/web.xml
+++ b/src/main/webapp/WEB-INF/web.xml
@@ -6,12 +6,6 @@
     <res-auth>Container</res-auth>
   </resource-ref>
 
-  <resource-ref>
-    <res-ref-name>mail/Outgoing</res-ref-name>
-    <res-type>javax.mail.Session</res-type>
-    <res-auth>Container</res-auth>
-  </resource-ref>
-
   <filter>
     <filter-name>UrlRewrite</filter-name>
     <filter-class>com.google.gerrit.server.UrlRewriteFilter</filter-class>