1 /* 2 * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 package org.openjdk.skara.email; 24 25 import java.time.ZonedDateTime; 26 import java.time.format.*; 27 import java.time.temporal.ChronoUnit; 28 import java.util.*; 29 import java.util.regex.Pattern; 30 import java.util.stream.Collectors; 31 32 public class Email { 33 private final EmailAddress id; 34 private final ZonedDateTime date; 35 private final List<EmailAddress> recipients; 36 private final EmailAddress author; 37 private final EmailAddress sender; 38 private final String subject; 39 private final String body; 40 private final Map<String, String> headers; 41 42 private final static Pattern mboxMessageHeaderBodyPattern = Pattern.compile( 43 "\\R{2}", Pattern.MULTILINE); 44 private final static Pattern mboxMessageHeaderPattern = Pattern.compile( 45 "^([-\\w]+): ((?:.(?!\\R\\w))*.)", Pattern.MULTILINE | Pattern.DOTALL); 46 47 Email(EmailAddress id, ZonedDateTime date, List<EmailAddress> recipients, EmailAddress author, EmailAddress sender, String subject, String body, Map<String, String> headers) { 48 this.id = id; 49 this.date = date.truncatedTo(ChronoUnit.SECONDS); 50 this.recipients = new ArrayList<>(recipients); 51 this.sender = sender; 52 this.subject = subject; 53 this.body = body; 54 this.author = author; 55 this.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 56 this.headers.putAll(headers); 57 } 58 59 private static class MboxMessage { 60 Map<String, String> headers; 61 String body; 62 } 63 64 private static MboxMessage parseMboxMessage(String message) { 65 var ret = new MboxMessage(); 66 67 var parts = mboxMessageHeaderBodyPattern.split(message, 2); 68 var headers = mboxMessageHeaderPattern.matcher(parts[0]).results() 69 .collect(Collectors.toMap(match -> match.group(1), 70 match -> match.group(2) 71 .replaceAll("\\R", ""))); 72 ret.headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 73 ret.headers.putAll(headers); 74 ret.body = parts[1].stripTrailing(); 75 return ret; 76 } 77 78 private static final Pattern redundantTimeZonePattern = Pattern.compile("^(.*[-+\\d{4}]) \\(\\w+\\)$"); 79 80 public static Email parse(String raw) { 81 var message = parseMboxMessage(raw); 82 83 var id = EmailAddress.parse(message.headers.get("Message-Id")); 84 var unparsedDate = message.headers.get("Date"); 85 var redundantTimeZonePatternMatcher = redundantTimeZonePattern.matcher(unparsedDate); 86 if (redundantTimeZonePatternMatcher.matches()) { 87 unparsedDate = redundantTimeZonePatternMatcher.group(1); 88 } 89 var date = ZonedDateTime.parse(unparsedDate, DateTimeFormatter.RFC_1123_DATE_TIME); 90 var subject = message.headers.get("Subject"); 91 var author = EmailAddress.parse(message.headers.get("From")); 92 var sender = author; 93 if (message.headers.containsKey("Sender")) { 94 sender = EmailAddress.parse(message.headers.get("Sender")); 95 } 96 List<EmailAddress> recipients; 97 if (message.headers.containsKey("To")) { 98 recipients = Arrays.stream(message.headers.get("To").split(",")) 99 .map(EmailAddress::parse) 100 .collect(Collectors.toList()); 101 } else { 102 recipients = List.of(); 103 } 104 105 // Remove all known headers 106 var filteredHeaders = message.headers.entrySet().stream() 107 .filter(entry -> !entry.getKey().equalsIgnoreCase("Message-Id")) 108 .filter(entry -> !entry.getKey().equalsIgnoreCase("Date")) 109 .filter(entry -> !entry.getKey().equalsIgnoreCase("Subject")) 110 .filter(entry -> !entry.getKey().equalsIgnoreCase("From")) 111 .filter(entry -> !entry.getKey().equalsIgnoreCase("Sender")) 112 .filter(entry -> !entry.getKey().equalsIgnoreCase("To")) 113 .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); 114 115 return new Email(id, date, recipients, author, sender, subject, message.body, filteredHeaders); 116 } 117 118 public static EmailBuilder create(EmailAddress author, String subject, String body) { 119 return new EmailBuilder(author, subject, body); 120 } 121 122 public static EmailBuilder create(String subject, String body) { 123 return new EmailBuilder(subject, body); 124 } 125 126 public static EmailBuilder from(Email email) { 127 return new EmailBuilder(email.author, email.subject, email.body) 128 .sender(email.sender) 129 .recipients(email.recipients) 130 .id(email.id) 131 .date(email.date) 132 .headers(email.headers); 133 } 134 135 public static EmailBuilder reply(Email parent, String subject, String body) { 136 var references = parent.id().toString(); 137 if (parent.hasHeader("References")) { 138 references = parent.headerValue("References") + " " + references; 139 } 140 141 return new EmailBuilder(subject, body) 142 .header("In-Reply-To", parent.id().toString()) 143 .header("References", references); 144 } 145 146 @Override 147 public boolean equals(Object o) { 148 if (this == o) { 149 return true; 150 } 151 if (o == null || getClass() != o.getClass()) { 152 return false; 153 } 154 Email email = (Email) o; 155 return id.equals(email.id) && 156 date.toEpochSecond() == email.date.toEpochSecond() && 157 recipients.equals(email.recipients) && 158 author.equals(email.author) && 159 sender.equals(email.sender) && 160 subject.equals(email.subject) && 161 body.equals(email.body) && 162 headers.equals(email.headers); 163 } 164 165 @Override 166 public int hashCode() { 167 return Objects.hash(id, date.toEpochSecond(), recipients, author, sender, subject, body, headers); 168 } 169 170 public EmailAddress id() { 171 return id; 172 } 173 174 public List<EmailAddress> recipients() { 175 return new ArrayList<>(recipients); 176 } 177 178 public EmailAddress author() { 179 return author; 180 } 181 182 public EmailAddress sender() { 183 return sender; 184 } 185 186 public ZonedDateTime date() { 187 return date; 188 } 189 190 public String subject() { 191 return subject; 192 } 193 194 public String body() { 195 return body; 196 } 197 198 public Set<String> headers() { 199 return new HashSet<>(headers.keySet()); 200 } 201 202 public boolean hasHeader(String header) { 203 return headers.containsKey(header); 204 } 205 206 public String headerValue(String header) { 207 return headers.get(header); 208 } 209 210 @Override 211 public String toString() { 212 return "Email{" + 213 "id='" + id + '\'' + 214 ", date=" + date + 215 ", recipients=" + recipients + 216 ", author=" + author + 217 ", sender=" + sender + 218 ", subject='" + subject + '\'' + 219 ", body='" + body + '\'' + 220 ", headers=" + headers + 221 '}'; 222 } 223 }