001package edu.pdx.cs410J.grader;
002
003import com.google.common.annotations.VisibleForTesting;
004import com.sun.mail.util.MailSSLSocketFactory;
005import jakarta.mail.*;
006import org.slf4j.Logger;
007import org.slf4j.LoggerFactory;
008
009import java.io.IOException;
010import java.security.GeneralSecurityException;
011import java.util.ArrayList;
012import java.util.List;
013import java.util.Properties;
014
015public class GraderEmailAccount {
016  private final Logger logger = LoggerFactory.getLogger(this.getClass().getPackage().getName());
017
018  private final String password;
019  private final String userName;
020  private final String emailServerHostName;
021  private final int emailServerPort;
022  private final boolean trustLocalhostSSL;
023  private final StatusLogger statusLogger;
024
025  public GraderEmailAccount(String userName, String password, StatusLogger statusLogger) {
026    this("imap.gmail.com", 993, userName, password, false, statusLogger);
027  }
028
029  @VisibleForTesting
030  public GraderEmailAccount(String emailServerHostName, int emailServerPort, String userName, String password, boolean trustLocalhostSSL, StatusLogger statusLogger) {
031    this.userName = userName;
032    this.password = password;
033    this.emailServerHostName = emailServerHostName;
034    this.emailServerPort = emailServerPort;
035    this.trustLocalhostSSL = trustLocalhostSSL;
036    this.statusLogger = statusLogger;
037  }
038
039  private void fetchAttachmentsFromUnreadMessagesInFolder(Folder folder, EmailAttachmentProcessor processor) {
040    try {
041      logStatus("Getting messages from \"%s\" folder", folder.getFullName());
042      Message[] messages = folder.getMessages();
043
044      FetchProfile profile = new FetchProfile();
045      profile.add(FetchProfile.Item.ENVELOPE);
046      profile.add(FetchProfile.Item.FLAGS);
047
048      folder.fetch(messages, profile);
049
050      for (int i = 0, messagesLength = messages.length; i < messagesLength; i++) {
051        Message message = messages[i];
052        logStatus("Processing message %d of %d from %s", i + 1, messages.length, message.getFrom()[0]);
053        if (isUnread(message)) {
054          fetchAttachmentsFromUnreadMessage(message, processor);
055        }
056      }
057
058    } catch (MessagingException | IOException ex ) {
059      throw new IllegalStateException("While printing unread messages", ex);
060    }
061  }
062
063  @VisibleForTesting
064  protected void fetchAttachmentsFromUnreadMessage(Message message, EmailAttachmentProcessor processor) throws MessagingException, IOException {
065    printMessageInformation(message);
066    if (isMultipartMessage(message)) {
067      processAttachments(message, processor);
068
069    } else if (processor.hasSupportedContentType(message)) {
070       processSinglePartBody(message, processor, message.getContentType());
071
072    } else {
073      warnOfUnexpectedMessage(message, "Fetched a message that wasn't multipart: " + message.getContentType());
074    }
075  }
076
077  private void processSinglePartBody(Message message, EmailAttachmentProcessor processor, String contentType) throws IOException, MessagingException {
078    processor.processAttachment(message, "SinglePartBody", message.getInputStream(), contentType);
079  }
080
081  private void logStatus(String format, Object... args) {
082    this.logStatus(String.format(format, args));
083  }
084
085  private void logStatus(String statusMessage) {
086    this.statusLogger.logStatus(statusMessage);
087  }
088
089  private void processAttachments(Message message, EmailAttachmentProcessor processor) throws MessagingException, IOException {
090    Multipart parts;
091    try {
092      parts = (Multipart) message.getContent();
093    } catch (IOException ex) {
094      throw new MessagingException("While getting content", ex);
095    }
096
097    debug("  Part count: " + parts.getCount());
098
099    if (parts.getCount() <= 0) {
100      warnOfUnexpectedMessage(message, "Fetched a message that has no attachments");
101    }
102
103    for (String contentType : processor.getSupportedContentTypes()) {
104      for (BodyPart part : getBodyPartsInReverseOrder(parts)) {
105        if (attemptToProcessPart(message, part, processor, contentType)) {
106          return;
107        }
108      }
109    }
110
111    warnOfUnexpectedMessage(message, "Could not process of any attachments");
112  }
113
114  private boolean attemptToProcessPart(Message message, BodyPart part, EmailAttachmentProcessor processor, String supportedContentType) throws MessagingException, IOException {
115    if (partHasContentType(part, supportedContentType)) {
116      debug("    Processing attachment of type " + part.getContentType());
117      processAttachmentFromPart(message, part, processor, part.getContentType());
118      return true;
119
120    } else if (partIsMultiPart(part)) {
121      debug("    Attempting to process attachment of type " + part.getContentType());
122
123      for (BodyPart subpart : getBodyPartsInReverseOrder((Multipart) part.getContent())) {
124        if (attemptToProcessPart(message, subpart, processor, supportedContentType)) {
125          return true;
126        }
127      }
128
129    } else {
130      debug("    Skipping attachment of type " + part.getContentType());
131    }
132    return false;
133  }
134
135  private List<BodyPart> getBodyPartsInReverseOrder(Multipart parts) throws MessagingException {
136    List<BodyPart> list = new ArrayList<>(parts.getCount());
137    for (int i = parts.getCount() - 1; i >= 0; i--) {
138      list.add(parts.getBodyPart(i));
139    }
140
141    return list;
142  }
143
144  private boolean partIsMultiPart(BodyPart part) throws IOException, MessagingException {
145    return part.getContent() instanceof Multipart;
146  }
147
148  private boolean partHasContentType(BodyPart part, String contentType) throws MessagingException {
149    return part.getContentType().toUpperCase().contains(contentType.toUpperCase());
150  }
151
152  private void processAttachmentFromPart(Message message, BodyPart part, EmailAttachmentProcessor processor, String contentType) throws MessagingException, IOException {
153    String fileName = part.getFileName();
154    processor.processAttachment(message, fileName, part.getInputStream(), contentType);
155  }
156
157  private void warnOfUnexpectedMessage(Message message, String description) throws MessagingException {
158    debug(description);
159    printMessageDetails(message);
160  }
161
162  private boolean isMultipartMessage(Message message) throws MessagingException {
163    return message.isMimeType("multipart/*");
164  }
165
166  private boolean isUnread(Message message) throws MessagingException {
167    return !message.getFlags().contains(Flags.Flag.SEEN);
168  }
169
170  private void printMessageInformation(Message message) throws MessagingException {
171    debug("Message");
172    printMessageDetails(message);
173  }
174
175  private void printMessageDetails(Message message) throws MessagingException {
176    debug("  To: " + addresses(message.getRecipients(Message.RecipientType.TO)));
177    debug("  From: " + addresses(message.getFrom()));
178    debug("  Subject: " + message.getSubject());
179    debug("  Sent: " + message.getSentDate());
180    debug("  Flags: " + flags(message.getFlags()));
181    debug("  Content Type: " + message.getContentType());
182  }
183
184  private StringBuilder flags(Flags flags) {
185    StringBuilder sb = new StringBuilder();
186    systemFlags(flags, sb);
187    return sb;
188  }
189
190  private void systemFlags(Flags flags, StringBuilder sb) {
191    Flags.Flag[] systemFlags = flags.getSystemFlags();
192    for (int i = 0; i < systemFlags.length; i++) {
193      Flags.Flag flag = systemFlags[i];
194      if (flag == Flags.Flag.ANSWERED) {
195        sb.append("ANSWERED");
196
197      } else if (flag == Flags.Flag.DELETED) {
198        sb.append("DELETED");
199
200      } else if (flag == Flags.Flag.DRAFT) {
201        sb.append("DRAFT");
202
203      } else if (flag == Flags.Flag.FLAGGED) {
204        sb.append("FLAGGED");
205
206      } else if (flag == Flags.Flag.RECENT) {
207        sb.append("RECENT");
208
209      } else if (flag == Flags.Flag.SEEN) {
210        sb.append("SEEN");
211
212      } else if (flag == Flags.Flag.USER) {
213        sb.append("USER");
214
215      } else {
216        sb.append("UNKNOWN");
217      }
218
219      if (i > systemFlags.length - 1) {
220        sb.append(", ");
221      }
222    }
223  }
224
225  private String addresses(Address[] addresses) {
226    if (addresses == null) {
227      return "<None>";
228    }
229
230    StringBuilder sb = new StringBuilder();
231    for (int i = 0; i < addresses.length; i++) {
232      Address address = addresses[i];
233      sb.append(address.toString());
234      if (i > addresses.length - 1) {
235        sb.append(", ");
236      }
237    }
238
239    return sb.toString();
240  }
241
242  private void printFolderInformation(Folder folder) {
243    try {
244      debug("Folder: " + folder.getFullName());
245      debug("Message count: " + folder.getMessageCount());
246      debug("Unread messages: " + folder.getUnreadMessageCount());
247      debug("New messages: " + folder.getNewMessageCount());
248
249    } catch (MessagingException ex) {
250      throw new IllegalStateException("While getting folder information", ex);
251    }
252  }
253
254  private void debug(String message) {
255    this.logger.debug(message);
256  }
257
258  private Folder openFolder(Store store, String folderName) {
259    try {
260      Folder folder = store.getDefaultFolder();
261      checkForValidFolder(folder, "Default");
262      folder = folder.getFolder(folderName);
263      folder.open(Folder.READ_WRITE);
264      return folder;
265
266    } catch (MessagingException ex) {
267      throw new IllegalStateException("While opening project submissions folder", ex);
268    }
269  }
270
271  private void checkForValidFolder(Folder folder, String folderName) {
272    if (folder == null) {
273      throw new IllegalStateException("Folder \"" + folderName + "\" does not exist");
274    }
275
276  }
277
278  private Store connectToIMAPServer() {
279    try {
280      Properties props = new Properties();
281
282      if (this.trustLocalhostSSL) {
283        MailSSLSocketFactory socketFactory= new MailSSLSocketFactory();
284        socketFactory.setTrustedHosts("127.0.0.1", "localhost");
285        props.put("mail.imaps.ssl.socketFactory", socketFactory);
286      }
287
288      Session session = Session.getInstance(props, null);
289      Store store = session.getStore("imaps");
290      store.connect(this.emailServerHostName, this.emailServerPort, this.userName, this.password);
291      return store;
292
293    } catch (MessagingException | GeneralSecurityException ex) {
294      throw new IllegalStateException("While connecting to " + this.emailServerHostName + ":" + this.emailServerPort, ex);
295    }
296  }
297
298  public void fetchAttachmentsFromUnreadMessagesInFolder(String folderName, EmailAttachmentProcessor processor) {
299    Store store = connectToIMAPServer();
300    Folder folder = openFolder(store, folderName);
301    printFolderInformation(folder);
302
303    fetchAttachmentsFromUnreadMessagesInFolder(folder, processor);
304
305    try {
306      folder.close(false);
307      store.close();
308
309    } catch (MessagingException ex) {
310      throw new IllegalStateException("While closing folder and store", ex);
311    }
312  }
313
314  public interface StatusLogger {
315    void logStatus(String statusMessage);
316  }
317}