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}