001/*
002 * acme4j - Java ACME client
003 *
004 * Copyright (C) 2021 Richard "Shred" Körber
005 *   http://acme4j.shredzone.org
006 *
007 * Licensed under the Apache License, Version 2.0 (the "License");
008 * you may not use this file except in compliance with the License.
009 *
010 * This program is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
013 */
014package org.shredzone.acme4j.smime.email;
015
016import static java.util.Objects.requireNonNull;
017import static jakarta.mail.Message.RecipientType.TO;
018import static org.shredzone.acme4j.smime.email.ResponseBodyGenerator.RESPONSE_BODY_TYPE;
019
020import java.util.Properties;
021
022import edu.umd.cs.findbugs.annotations.Nullable;
023import jakarta.mail.Address;
024import jakarta.mail.Message;
025import jakarta.mail.MessagingException;
026import jakarta.mail.Session;
027import jakarta.mail.internet.MimeMessage;
028
029/**
030 * A helper for creating an email response to the "challenge" email.
031 * <p>
032 * According to RFC-8823, the response email <em>must</em> be DKIM signed. This is
033 * <em>not</em> done by the response generator, but must be done by the outbound MTA.
034 *
035 * @see <a href="https://datatracker.ietf.org/doc/html/rfc8823">RFC 8823</a>
036 * @since 2.12
037 */
038public class ResponseGenerator {
039    private static final int LINE_LENGTH = 72;
040    private static final String CRLF = "\r\n";
041
042    private final EmailProcessor processor;
043    private ResponseBodyGenerator generator = this::defaultBodyGenerator;
044    private @Nullable String header;
045    private @Nullable String footer;
046
047    /**
048     * Creates a new {@link ResponseGenerator}.
049     *
050     * @param processor
051     *         {@link EmailProcessor} of the challenge email.
052     */
053    public ResponseGenerator(EmailProcessor processor) {
054        this.processor = requireNonNull(processor, "processor");
055    }
056
057    /**
058     * Adds a custom header to the response mail body.
059     * <p>
060     * There is no need to set a header, since the response email is usually not read by
061     * humans. If a header is set, it must contain ASCII encoded plain text.
062     *
063     * @param header
064     *         Header text to be used, or {@code null} if no header is to be used.
065     * @return itself
066     */
067    public ResponseGenerator withHeader(@Nullable String header) {
068        if (header != null && !header.endsWith(CRLF)) {
069            this.header = header.concat(CRLF);
070        } else {
071            this.header = header;
072        }
073        return this;
074    }
075
076    /**
077     * Adds a custom footer to the response mail body.
078     * <p>
079     * There is no need to set a footer, since the response email is usually not read by
080     * humans. If a footer is set, it must contain ASCII encoded plain text.
081     *
082     * @param footer
083     *         Footer text to be used, or {@code null} if no footer is to be used.
084     * @return itself
085     */
086    public ResponseGenerator withFooter(@Nullable String footer) {
087        this.footer = footer;
088        return this;
089    }
090
091    /**
092     * Sets a {@link ResponseBodyGenerator} that is used for generating a response body.
093     * <p>
094     * Use this generator to individually style the email body, for example to use a
095     * multipart body. However, be aware that the response mail is evaluated by a machine,
096     * and usually not read by humans, so the body should be designed as simple as
097     * possible.
098     * <p>
099     * The default body generator will just concatenate the header, the armored key
100     * authorization body, and the footer.
101     *
102     * @param generator
103     *         {@link ResponseBodyGenerator} to be used, or {@code null} to use the
104     *         default one.
105     * @return itself
106     */
107    public ResponseGenerator withGenerator(@Nullable ResponseBodyGenerator generator) {
108        this.generator = generator != null ? generator : this::defaultBodyGenerator;
109        return this;
110    }
111
112    /**
113     * Generates the response email.
114     * <p>
115     * A simple default mail session is used for generation.
116     *
117     * @return Generated {@link Message}.
118     * @since 2.16
119     */
120    public Message generateResponse() throws MessagingException {
121        return generateResponse(Session.getDefaultInstance(new Properties()));
122    }
123
124    /**
125     * Generates the response email.
126     * <p>
127     * Note that according to RFC-8823, this message must have a valid DKIM or S/MIME
128     * signature. This is <em>not</em> done here, but usually performed by the outbound
129     * MTA.
130     *
131     * @param session
132     *         {@code javax.mail} {@link Session} to be used for this mail.
133     * @return Generated {@link Message}.
134     */
135    public Message generateResponse(Session session) throws MessagingException {
136        var response = new MimeMessage(requireNonNull(session, "session"));
137
138        response.setSubject("Re: ACME: " + processor.getToken1());
139        response.setFrom(processor.getRecipient());
140
141        if (!processor.getReplyTo().isEmpty()) {
142            for (var rto : processor.getReplyTo()) {
143                response.addRecipient(TO, rto);
144            }
145        } else {
146            response.addRecipients(TO, new Address[] {processor.getSender()});
147        }
148
149        if (processor.getMessageId().isPresent()) {
150            response.setHeader("In-Reply-To", processor.getMessageId().get());
151        }
152
153        var wrappedAuth = processor.getAuthorization()
154                .replaceAll("(.{" + LINE_LENGTH + "})", "$1" + CRLF);
155        var responseBody = new StringBuilder();
156        responseBody.append("-----BEGIN ACME RESPONSE-----").append(CRLF);
157        responseBody.append(wrappedAuth);
158        if (!wrappedAuth.endsWith(CRLF)) {
159            responseBody.append(CRLF);
160        }
161        responseBody.append("-----END ACME RESPONSE-----").append(CRLF);
162
163        generator.setContent(response, responseBody.toString());
164        return response;
165    }
166
167    /**
168     * The default body generator. It just sets the response body, optionally framed by
169     * the given header and footer.
170     *
171     * @param response
172     *         response {@link Message} to fill.
173     * @param responseBody
174     *         Response body that must be added to the message.
175     */
176    private void defaultBodyGenerator(Message response, String responseBody)
177            throws MessagingException {
178        var body = new StringBuilder();
179        if (header != null) {
180            body.append(header);
181        }
182        body.append(responseBody);
183        if (footer != null) {
184            body.append(footer);
185        }
186        response.setContent(body.toString(), RESPONSE_BODY_TYPE);
187    }
188
189}