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>