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 = MimeText.decode(message.headers.get("Subject"));
 91         var author = EmailAddress.parse(MimeText.decode(message.headers.get("From")));
 92         var sender = author;
 93         if (message.headers.containsKey("Sender")) {
 94             sender = EmailAddress.parse(MimeText.decode(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(MimeText::decode)
100                                .map(EmailAddress::parse)
101                                .collect(Collectors.toList());
102         } else {
103             recipients = List.of();
104         }
105 
106         // Remove all known headers
107         var filteredHeaders = message.headers.entrySet().stream()
108                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Message-Id"))
109                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Date"))
110                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Subject"))
111                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("From"))
112                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("Sender"))
113                                              .filter(entry -> !entry.getKey().equalsIgnoreCase("To"))
114                                              .collect(Collectors.toMap(Map.Entry::getKey,
115                                                                        entry -> MimeText.decode(entry.getValue())));
116 
117         return new Email(id, date, recipients, author, sender, subject, MimeText.decode(message.body), filteredHeaders);
118     }
119 
120     public static EmailBuilder create(EmailAddress author, String subject, String body) {
121         return new EmailBuilder(author, subject, body);
122     }
123 
124     public static EmailBuilder create(String subject, String body) {
125         return new EmailBuilder(subject, body);
126     }
127 
128     public static EmailBuilder from(Email email) {
129         return new EmailBuilder(email.author, email.subject, email.body)
130                 .sender(email.sender)
131                 .recipients(email.recipients)
132                 .id(email.id)
133                 .date(email.date)
134                 .headers(email.headers);
135     }
136 
137     public static EmailBuilder reply(Email parent, String subject, String body) {
138         var references = parent.id().toString();
139         if (parent.hasHeader("References")) {
140             references = parent.headerValue("References") + " " + references;
141         }
142 
143         return new EmailBuilder(subject, body)
144                 .header("In-Reply-To", parent.id().toString())
145                 .header("References", references);
146     }
147 
148     @Override
149     public boolean equals(Object o) {
150         if (this == o) {
151             return true;
152         }
153         if (o == null || getClass() != o.getClass()) {
154             return false;
155         }
156         Email email = (Email) o;
157         return id.equals(email.id) &&
158                 date.toEpochSecond() == email.date.toEpochSecond() &&
159                 recipients.equals(email.recipients) &&
160                 author.equals(email.author) &&
161                 sender.equals(email.sender) &&
162                 subject.equals(email.subject) &&
163                 body.equals(email.body) &&
164                 headers.equals(email.headers);
165     }
166 
167     @Override
168     public int hashCode() {
169         return Objects.hash(id, date.toEpochSecond(), recipients, author, sender, subject, body, headers);
170     }
171 
172     public EmailAddress id() {
173         return id;
174     }
175 
176     public List<EmailAddress> recipients() {
177         return new ArrayList<>(recipients);
178     }
179 
180     public EmailAddress author() {
181         return author;
182     }
183 
184     public EmailAddress sender() {
185         return sender;
186     }
187 
188     public ZonedDateTime date() {
189         return date;
190     }
191 
192     public String subject() {
193         return subject;
194     }
195 
196     public String body() {
197         return body;
198     }
199 
200     public Set<String> headers() {
201         return new HashSet<>(headers.keySet());
202     }
203 
204     public boolean hasHeader(String header) {
205         return headers.containsKey(header);
206     }
207 
208     public String headerValue(String header) {
209         return headers.get(header);
210     }
211 
212     @Override
213     public String toString() {
214         return "Email{" +
215                 "id='" + id + '\'' +
216                 ", date=" + date +
217                 ", recipients=" + recipients +
218                 ", author=" + author +
219                 ", sender=" + sender +
220                 ", subject='" + subject + '\'' +
221                 ", body='" + body + '\'' +
222                 ", headers=" + headers +
223                 '}';
224     }
225 }