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