Add POP3 implementation for receiving emails

This change adds code to retrieve and delete emails using POP3 as well
as a lightweight implementation to receive the Message-ID from a raw
email.

It uses Greenmail for integration testing and tests if Gerrit can delete
emails which implies that it can also read emails.

Change-Id: Ifa20a5d77428759d85b24b2fe2c161e72f81c991
This commit is contained in:
Patrick Hiesel
2016-10-24 14:36:48 +02:00
parent 8f0fabf9dc
commit 63eef7ae4e
7 changed files with 233 additions and 4 deletions

View File

@@ -49,10 +49,12 @@ java_library2(
'//lib:truth',
],
deps = PROVIDED + [ # We want these deps to be exported_deps
'//lib/greenmail:greenmail',
'//lib:gwtorm',
'//lib/guice:guice',
'//lib/guice:guice-assistedinject',
'//lib/guice:guice-servlet',
'//lib/mail:mail',
],
visibility = ['//visibility:public'],
)

View File

@@ -0,0 +1,7 @@
include_defs('//gerrit-acceptance-tests/tests.defs')
acceptance_tests(
group = 'server_mail',
srcs = glob(['*IT.java']),
labels = ['server'],
)

View File

@@ -0,0 +1,11 @@
load("//gerrit-acceptance-tests:tests.bzl", "acceptance_tests")
acceptance_tests(
srcs = glob(["*IT.java"]),
group = "server_mail",
labels = ["server"],
deps = [
"//lib/greenmail",
"//lib/mail",
],
)

View File

@@ -0,0 +1,95 @@
// Copyright (C) 2016 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.acceptance.server.mail;
import static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GerritConfig;
import com.google.gerrit.acceptance.GerritConfigs;
import com.google.gerrit.acceptance.NoHttpd;
import com.google.gerrit.server.mail.receive.MailReceiver;
import com.google.gerrit.testutil.ConfigSuite;
import com.google.inject.Inject;
import com.icegreen.greenmail.junit.GreenMailRule;
import com.icegreen.greenmail.user.GreenMailUser;
import com.icegreen.greenmail.util.GreenMail;
import com.icegreen.greenmail.util.GreenMailUtil;
import com.icegreen.greenmail.util.ServerSetupTest;
import org.eclipse.jgit.lib.Config;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import javax.mail.internet.MimeMessage;
@NoHttpd
@RunWith(ConfigSuite.class)
public class MailIT extends AbstractDaemonTest {
private final static String RECEIVEEMAIL = "receiveemail";
private final static String HOST = "localhost";
private final static String USERNAME = "user@domain.com";
private final static String PASSWORD = "password";
private final static String PROTOCOL = "POP3";
@Inject
private MailReceiver mailReceiver;
@Inject
private GreenMail greenMail;
@Rule
public final GreenMailRule mockPop3Server = new GreenMailRule(
ServerSetupTest.SMTP_POP3_IMAP);
@ConfigSuite.Default
public static Config pop3Config() {
Config cfg = new Config();
cfg.setString(RECEIVEEMAIL, null, "host", HOST);
cfg.setString(RECEIVEEMAIL, null, "port", "3110");
cfg.setString(RECEIVEEMAIL, null, "username", USERNAME);
cfg.setString(RECEIVEEMAIL, null, "password", PASSWORD);
cfg.setString(RECEIVEEMAIL, null, "protocol", PROTOCOL);
cfg.setString(RECEIVEEMAIL, null, "fetchInterval", "99");
return cfg;
}
@Test
public void testDelete() throws Exception {
GreenMailUser user = mockPop3Server.setUser(USERNAME, USERNAME, PASSWORD);
user.deliver(createSimpleMessage());
assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(1);
// Let Gerrit handle emails
mailReceiver.handleEmails();
// Check that the message is still present
assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(1);
// Mark the message for deletion
mailReceiver.requestDeletion(
mockPop3Server.getReceivedMessages()[0].getMessageID());
// Let Gerrit handle emails
mailReceiver.handleEmails();
// Check that the message was deleted
assertThat(mockPop3Server.getReceivedMessages().length).isEqualTo(0);
}
private MimeMessage createSimpleMessage() {
return GreenMailUtil
.createTextEmail(USERNAME, "from@localhost.com", "subject",
"body",
greenMail.getImap().getServerSetup());
}
}

View File

@@ -31,7 +31,7 @@ public class ImapMailReceiver extends MailReceiver {
* where deletion is pending, read new email and close the connection.
*/
@Override
protected synchronized void handleEmails() {
public synchronized void handleEmails() {
// TODO(hiesel) Implement.
}
}

View File

@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail.receive;
import com.google.common.annotations.VisibleForTesting;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.lifecycle.LifecycleModule;
import com.google.gerrit.server.mail.EmailSettings;
@@ -100,5 +101,6 @@ public abstract class MailReceiver implements LifecycleListener {
* handleEmails will open a connection to the mail server, remove emails
* where deletion is pending, read new email and close the connection.
*/
protected abstract void handleEmails();
@VisibleForTesting
public abstract void handleEmails();
}

View File

@@ -14,12 +14,29 @@
package com.google.gerrit.server.mail.receive;
import com.google.common.primitives.Ints;
import com.google.gerrit.server.mail.EmailSettings;
import com.google.gerrit.server.mail.Encryption;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.net.pop3.POP3Client;
import org.apache.commons.net.pop3.POP3MessageInfo;
import org.apache.commons.net.pop3.POP3SClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Singleton
public class Pop3MailReceiver extends MailReceiver {
private static final Logger log =
LoggerFactory.getLogger(Pop3MailReceiver.class);
@Inject
public Pop3MailReceiver(EmailSettings mailSettings) {
super(mailSettings);
@@ -30,7 +47,102 @@ public class Pop3MailReceiver extends MailReceiver {
* where deletion is pending, read new email and close the connection.
*/
@Override
protected synchronized void handleEmails() {
// TODO(hiesel) Implement.
public synchronized void handleEmails() {
POP3Client pop3;
if (mailSettings.encryption != Encryption.NONE) {
pop3 = new POP3SClient(mailSettings.encryption.name());
} else {
pop3 = new POP3Client();
}
if (mailSettings.port > 0) {
pop3.setDefaultPort(mailSettings.port);
}
try {
pop3.connect(mailSettings.host);
} catch (IOException e) {
log.error("Could not connect to POP3 email server", e);
return;
}
try {
try {
if (!pop3.login(mailSettings.username, mailSettings.password)) {
log.error("Could not login to POP3 email server."
+ " Check username and password");
return;
}
try {
POP3MessageInfo[] messages = pop3.listMessages();
if (messages == null) {
log.error("Could not retrieve message list via POP3");
return;
}
log.info("Received " + messages.length + " messages via POP3");
// Fetch messages
List<MailMessage> mailMessages = new ArrayList<>();
for (POP3MessageInfo msginfo : messages) {
if (msginfo == null) {
// Message was deleted
continue;
}
Reader reader = pop3.retrieveMessage(msginfo.number);
if (reader == null) {
log.error("Could not retrieve POP3 message header for message " +
msginfo.identifier);
return;
}
int[] message = fetchMessage(reader);
try {
MailMessage mailMessage = RawMailParser.parse(message);
// Delete messages where deletion is pending. This requires
// knowing the integer message ID of the email. We therefore parse
// the message first and extract the Message-ID specified in RFC
// 822 and delete the message if deletion is pending.
if (pendingDeletion.contains(mailMessage.id())) {
if (pop3.deleteMessage(msginfo.number)) {
pendingDeletion.remove(mailMessage.id());
} else {
log.error("Could not delete message " + msginfo.number);
}
} else {
// Process message further
mailMessages.add(mailMessage);
}
} catch (MailParsingException e) {
log.error("Could not parse message " + msginfo.number);
}
}
// TODO(hiesel) Call processing logic with mailMessages
} finally {
pop3.logout();
}
} finally {
pop3.disconnect();
}
} catch (IOException e) {
log.error("Error while issuing POP3 command", e);
}
}
public final int[] fetchMessage(Reader reader) throws IOException {
BufferedReader bufferedReader;
if (reader instanceof BufferedReader) {
bufferedReader = (BufferedReader) reader;
} else {
bufferedReader = new BufferedReader(reader);
}
try {
List<Integer> character = new ArrayList<>();
int ch;
while ((ch = bufferedReader.read()) != -1) {
character.add(ch);
}
return Ints.toArray(character);
} finally {
bufferedReader.close();
if (bufferedReader != reader) {
reader.close();
}
}
}
}