Line data Source code
1 : // Copyright (C) 2016 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.mail.receive; 16 : 17 : import com.google.common.flogger.FluentLogger; 18 : import com.google.gerrit.mail.MailMessage; 19 : import com.google.gerrit.mail.MailParsingException; 20 : import com.google.gerrit.mail.RawMailParser; 21 : import com.google.gerrit.server.git.WorkQueue; 22 : import com.google.gerrit.server.mail.EmailSettings; 23 : import com.google.gerrit.server.mail.Encryption; 24 : import com.google.inject.Inject; 25 : import com.google.inject.Singleton; 26 : import java.io.IOException; 27 : import java.util.ArrayList; 28 : import java.util.List; 29 : import org.apache.commons.net.imap.IMAPClient; 30 : import org.apache.commons.net.imap.IMAPSClient; 31 : 32 : @Singleton 33 : public class ImapMailReceiver extends MailReceiver { 34 1 : private static final FluentLogger logger = FluentLogger.forEnclosingClass(); 35 : 36 : private static final String INBOX_FOLDER = "INBOX"; 37 : 38 : @Inject 39 : ImapMailReceiver(EmailSettings mailSettings, MailProcessor mailProcessor, WorkQueue workQueue) { 40 1 : super(mailSettings, mailProcessor, workQueue); 41 1 : } 42 : 43 : /** 44 : * Opens a connection to the mail server, removes emails where deletion is pending, reads new 45 : * email and closes the connection. 46 : * 47 : * @param async determines if processing messages should happen asynchronously 48 : * @throws MailTransferException in case of a known transport failure 49 : * @throws IOException in case of a low-level transport failure 50 : */ 51 : @Override 52 : public synchronized void handleEmails(boolean async) throws MailTransferException, IOException { 53 : IMAPClient imap; 54 1 : if (mailSettings.encryption != Encryption.NONE) { 55 0 : imap = new IMAPSClient(mailSettings.encryption.name(), true); 56 : } else { 57 1 : imap = new IMAPClient(); 58 : } 59 1 : if (mailSettings.port > 0) { 60 1 : imap.setDefaultPort(mailSettings.port); 61 : } 62 : // Set a 30s timeout for each operation 63 1 : imap.setDefaultTimeout(30 * 1000); 64 1 : imap.connect(mailSettings.host); 65 : try { 66 1 : if (!imap.login(mailSettings.username, mailSettings.password)) { 67 0 : throw new MailTransferException("Could not login to IMAP server"); 68 : } 69 : try { 70 1 : if (!imap.select(INBOX_FOLDER)) { 71 0 : throw new MailTransferException("Could not select IMAP folder " + INBOX_FOLDER); 72 : } 73 : // Fetch just the internal dates first to know how many messages we 74 : // should fetch. 75 1 : if (!imap.fetch("1:*", "(INTERNALDATE)")) { 76 : // false indicates that there are no messages to fetch 77 0 : logger.atInfo().log("Fetched 0 messages via IMAP"); 78 0 : return; 79 : } 80 : // Format of reply is one line per email and one line to indicate 81 : // that the fetch was successful. 82 : // Example: 83 : // * 1 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)") 84 : // * 2 FETCH (INTERNALDATE "Mon, 24 Oct 2016 16:53:22 +0200 (CEST)") 85 : // AAAC OK FETCH completed. 86 1 : int numMessages = imap.getReplyStrings().length - 1; 87 1 : logger.atInfo().log("Fetched %d messages via IMAP", numMessages); 88 : // Fetch the full version of all emails 89 1 : List<MailMessage> mailMessages = new ArrayList<>(numMessages); 90 1 : for (int i = 1; i <= numMessages; i++) { 91 1 : if (imap.fetch(i + ":" + i, "(BODY.PEEK[])")) { 92 : // Obtain full reply 93 1 : String[] rawMessage = imap.getReplyStrings(); 94 1 : if (rawMessage.length < 2) { 95 0 : continue; 96 : } 97 : // First and last line are IMAP status codes. We have already 98 : // checked, that the fetch returned true (OK), so we safely ignore 99 : // those two lines. 100 1 : StringBuilder b = new StringBuilder(2 * (rawMessage.length - 2)); 101 1 : for (int j = 1; j < rawMessage.length - 1; j++) { 102 1 : if (j > 1) { 103 1 : b.append("\n"); 104 : } 105 1 : b.append(rawMessage[j]); 106 : } 107 : try { 108 1 : MailMessage mailMessage = RawMailParser.parse(b.toString()); 109 1 : if (pendingDeletion.contains(mailMessage.id())) { 110 : // Mark message as deleted 111 1 : if (imap.store(i + ":" + i, "+FLAGS", "(\\Deleted)")) { 112 1 : pendingDeletion.remove(mailMessage.id()); 113 : } else { 114 0 : logger.atSevere().log( 115 0 : "Could not mark mail message as deleted: %s", mailMessage.id()); 116 : } 117 : } else { 118 1 : mailMessages.add(mailMessage); 119 : } 120 0 : } catch (MailParsingException e) { 121 0 : logger.atSevere().withCause(e).log("Exception while parsing email after IMAP fetch"); 122 1 : } 123 1 : } else { 124 0 : logger.atSevere().log("IMAP fetch failed. Will retry in next fetch cycle."); 125 : } 126 : } 127 : // Permanently delete emails marked for deletion 128 1 : if (!imap.expunge()) { 129 0 : logger.atSevere().log("Could not expunge IMAP emails"); 130 : } 131 1 : dispatchMailProcessor(mailMessages, async); 132 : } finally { 133 1 : imap.logout(); 134 : } 135 : } finally { 136 1 : imap.disconnect(); 137 : } 138 1 : } 139 : }