MailSender.java 12.0 KB
Newer Older
1 2
package hudson.tasks;

3 4
import hudson.FilePath;
import hudson.Util;
5
import hudson.model.AbstractBuild;
6
import hudson.model.AbstractProject;
7 8 9 10 11 12 13 14
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.model.User;
import hudson.scm.ChangeLogSet;
import hudson.scm.ChangeLogSet.Entry;

import javax.mail.Address;
import javax.mail.Message;
15 16 17 18
import javax.mail.MessagingException;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
19 20 21
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
22 23 24 25 26 27 28 29 30
import java.io.StringWriter;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
31 32 33 34 35 36 37

/**
 * Core logic of sending out notification e-mail.
 *
 * @author Jesse Glick
 * @author Kohsuke Kawaguchi
 */
K
kohsuke 已提交
38
public class MailSender<P extends AbstractProject<P, B>, B extends AbstractBuild<P, B>> {
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
    /**
     * Whitespace-separated list of e-mail addresses that represent recipients.
     */
    private String recipients;

    /**
     * If true, only the first unstable build will be reported.
     */
    private boolean dontNotifyEveryUnstableBuild;

    /**
     * If true, individuals will receive e-mails regarding who broke the build.
     */
    private boolean sendToIndividuals;


    public MailSender(String recipients, boolean dontNotifyEveryUnstableBuild, boolean sendToIndividuals) {
        this.recipients = recipients;
        this.dontNotifyEveryUnstableBuild = dontNotifyEveryUnstableBuild;
        this.sendToIndividuals = sendToIndividuals;
    }

    public boolean execute(B build, BuildListener listener) throws InterruptedException {
        try {
            MimeMessage mail = getMail(build, listener);
K
kohsuke 已提交
64
            if (mail != null) {
65
                Address[] allRecipients = mail.getAllRecipients();
K
kohsuke 已提交
66 67
                if (allRecipients != null) {
                    StringBuffer buf = new StringBuffer("Sending e-mails to:");
68
                    for (Address a : allRecipients)
K
kohsuke 已提交
69 70 71
                        buf.append(' ').append(a);
                    listener.getLogger().println(buf);
                    Transport.send(mail);
72 73
                } else {
                    listener.getLogger().println("An attempt to send an e-mail"
K
kohsuke 已提交
74
                        + " to empty list of recipients, ignored.");
75 76 77
                }
            }
        } catch (MessagingException e) {
K
kohsuke 已提交
78
            e.printStackTrace(listener.error(e.getMessage()));
79 80 81 82 83 84
        }

        return true;
    }

    private MimeMessage getMail(B build, BuildListener listener) throws MessagingException, InterruptedException {
K
kohsuke 已提交
85
        if (build.getResult() == Result.FAILURE) {
86 87 88
            return createFailureMail(build, listener);
        }

K
kohsuke 已提交
89
        if (build.getResult() == Result.UNSTABLE) {
90
            B prev = build.getPreviousBuild();
K
kohsuke 已提交
91
            if (!dontNotifyEveryUnstableBuild)
92
                return createUnstableMail(build, listener);
K
kohsuke 已提交
93 94
            if (prev != null) {
                if (prev.getResult() == Result.SUCCESS)
95 96 97 98
                    return createUnstableMail(build, listener);
            }
        }

K
kohsuke 已提交
99
        if (build.getResult() == Result.SUCCESS) {
100
            B prev = build.getPreviousBuild();
K
kohsuke 已提交
101 102
            if (prev != null) {
                if (prev.getResult() == Result.FAILURE)
103
                    return createBackToNormalMail(build, "normal", listener);
K
kohsuke 已提交
104
                if (prev.getResult() == Result.UNSTABLE)
105 106 107 108 109 110 111 112 113 114
                    return createBackToNormalMail(build, "stable", listener);
            }
        }

        return null;
    }

    private MimeMessage createBackToNormalMail(B build, String subject, BuildListener listener) throws MessagingException {
        MimeMessage msg = createEmptyMail(build, listener);

K
kohsuke 已提交
115
        msg.setSubject(getSubject(build, "Hudson build is back to " + subject + ": "));
116
        StringBuffer buf = new StringBuffer();
K
kohsuke 已提交
117
        appendBuildUrl(build, buf);
118 119 120 121 122 123 124 125
        msg.setText(buf.toString());

        return msg;
    }

    private MimeMessage createUnstableMail(B build, BuildListener listener) throws MessagingException {
        MimeMessage msg = createEmptyMail(build, listener);

K
kohsuke 已提交
126
        msg.setSubject(getSubject(build, "Hudson build became unstable: "));
127
        StringBuffer buf = new StringBuffer();
K
kohsuke 已提交
128
        appendBuildUrl(build, buf);
129 130 131 132 133 134 135
        msg.setText(buf.toString());

        return msg;
    }

    private void appendBuildUrl(B build, StringBuffer buf) {
        String baseUrl = Mailer.DESCRIPTOR.getUrl();
K
kohsuke 已提交
136
        if (baseUrl != null) {
137
            buf.append("See ").append(baseUrl).append(Util.encode(build.getUrl())).append("changes\n\n");
138 139 140 141 142 143 144 145 146
        }
    }

    private MimeMessage createFailureMail(B build, BuildListener listener) throws MessagingException, InterruptedException {
        MimeMessage msg = createEmptyMail(build, listener);

        msg.setSubject(getSubject(build, "Build failed in Hudson: "));

        StringBuffer buf = new StringBuffer();
K
kohsuke 已提交
147
        appendBuildUrl(build, buf);
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235

        boolean firstChange = true;
        for (ChangeLogSet.Entry entry : build.getChangeSet()) {
            if (firstChange) {
                firstChange = false;
                buf.append("Changes:\n\n");
            }
            buf.append('[');
            buf.append(entry.getAuthor().getFullName());
            buf.append("] ");
            String m = entry.getMsg();
            buf.append(m);
            if (!m.endsWith("\n")) {
                buf.append('\n');
            }
            buf.append('\n');
        }

        buf.append("------------------------------------------\n");

        try {
            String log = build.getLog();
            String[] lines = log.split("\n");
            int start = 0;
            if (lines.length > MAX_LOG_LINES) {
                // Avoid sending enormous logs over email.
                // Interested users can always look at the log on the web server.
                buf.append("[...truncated " + (lines.length - MAX_LOG_LINES) + " lines...]\n");
                start = lines.length - MAX_LOG_LINES;
            }
            String workspaceUrl = null, artifactUrl = null;
            Pattern wsPattern = null;
            String baseUrl = Mailer.DESCRIPTOR.getUrl();
            if (baseUrl != null) {
                // Hyperlink local file paths to the repository workspace or build artifacts.
                // Note that it is possible for a failure mail to refer to a file using a workspace
                // URL which has already been corrected in a subsequent build. To fix, archive.
                workspaceUrl = baseUrl + Util.encode(build.getProject().getUrl()) + "ws/";
                artifactUrl = baseUrl + Util.encode(build.getUrl()) + "artifact/";
                FilePath ws = build.getProject().getWorkspace();
                // Match either file or URL patterns, i.e. either
                // c:\hudson\workdir\jobs\foo\workspace\src\Foo.java
                // file:/c:/hudson/workdir/jobs/foo/workspace/src/Foo.java
                // will be mapped to one of:
                // http://host/hudson/job/foo/ws/src/Foo.java
                // http://host/hudson/job/foo/123/artifact/src/Foo.java
                // Careful with path separator between $1 and $2:
                // workspaceDir will not normally end with one;
                // workspaceDir.toURI() will end with '/' if and only if workspaceDir.exists() at time of call
                wsPattern = Pattern.compile("(" +
                    quoteRegexp(ws.getRemote()) + "|" + quoteRegexp(ws.toURI().toString()) + ")[/\\\\]?([^:#\\s]*)");
            }
            for (int i = start; i < lines.length; i++) {
                String line = lines[i];
                if (wsPattern != null) {
                    // Perl: $line =~ s{$rx}{$path = $2; $path =~ s!\\\\!/!g; $workspaceUrl . $path}eg;
                    Matcher m = wsPattern.matcher(line);
                    int pos = 0;
                    while (m.find(pos)) {
                        String path = m.group(2).replace(File.separatorChar, '/');
                        String linkUrl = artifactMatches(path, build) ? artifactUrl : workspaceUrl;
                        // Append ' ' to make sure mail readers do not interpret following ':' as part of URL:
                        String prefix = line.substring(0, m.start()) + linkUrl + Util.encode(path) + ' ';
                        pos = prefix.length();
                        line = prefix + line.substring(m.end());
                        // XXX better style to reuse Matcher and fix offsets, but more work
                        m = wsPattern.matcher(line);
                    }
                }
                buf.append(line);
                buf.append('\n');
            }
        } catch (IOException e) {
            // somehow failed to read the contents of the log
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            buf.append("Failed to access build log\n\n").append(sw);
        }

        msg.setText(buf.toString());

        return msg;
    }

    private MimeMessage createEmptyMail(B build, BuildListener listener) throws MessagingException {
        MimeMessage msg = new MimeMessage(Mailer.DESCRIPTOR.createSession());
        // TODO: I'd like to put the URL to the page in here,
        // but how do I obtain that?
K
kohsuke 已提交
236
        msg.setContent("", "text/plain");
237 238 239
        msg.setFrom(new InternetAddress(Mailer.DESCRIPTOR.getAdminAddress()));
        msg.setSentDate(new Date());

240
        Set<InternetAddress> rcp = new LinkedHashSet<InternetAddress>();
241
        StringTokenizer tokens = new StringTokenizer(recipients);
K
kohsuke 已提交
242
        while (tokens.hasMoreTokens())
243
            rcp.add(new InternetAddress(tokens.nextToken()));
K
kohsuke 已提交
244
        if (sendToIndividuals) {
K
kohsuke 已提交
245 246 247 248 249 250
            if(debug) {
                int count = 0;
                for (Entry cs : build.getChangeSet())   count++;
                listener.getLogger().println("Trying to send e-mails to individuals who broke the build. sizeof(changeset)=="+count);
            }

251 252 253
            Set<User> users = new HashSet<User>();
            for (Entry change : build.getChangeSet()) {
                User a = change.getAuthor();
K
kohsuke 已提交
254
                if (users.add(a)) {
255
                    String adrs = a.getProperty(Mailer.UserProperty.class).getAddress();
K
kohsuke 已提交
256 257
                    if(debug)
                        listener.getLogger().println("  User "+a.getId()+" -> "+adrs);
K
kohsuke 已提交
258
                    if (adrs != null)
259 260
                        rcp.add(new InternetAddress(adrs));
                    else {
K
kohsuke 已提交
261
                        listener.getLogger().println("Failed to send e-mail to " + a.getFullName() + " because no e-mail address is known, and no default e-mail domain is configured");
262 263 264 265 266 267 268 269 270
                    }
                }
            }
        }
        msg.setRecipients(Message.RecipientType.TO, rcp.toArray(new InternetAddress[rcp.size()]));
        return msg;
    }

    private String getSubject(B build, String caption) {
271
        return caption + build.getProject().getFullDisplayName() + " #" + build.getNumber();
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294
    }

    /**
     * Copied from JDK5, to avoid 5.0 dependency.
     */
    private static String quoteRegexp(String s) {
        int slashEIndex = s.indexOf("\\E");
        if (slashEIndex == -1)
            return "\\Q" + s + "\\E";

        StringBuilder sb = new StringBuilder(s.length() * 2);
        sb.append("\\Q");
        int current = 0;
        while ((slashEIndex = s.indexOf("\\E", current)) != -1) {
            sb.append(s.substring(current, slashEIndex));
            current = slashEIndex + 2;
            sb.append("\\E\\\\E\\Q");
        }
        sb.append(s.substring(current, s.length()));
        sb.append("\\E");
        return sb.toString();
    }

K
kohsuke 已提交
295 296 297
    /**
     * Check whether a path (/-separated) will be archived.
     */
298 299 300 301 302 303 304
    protected boolean artifactMatches(String path, B build) {
        return false;
    }


    private static final Logger LOGGER = Logger.getLogger(MailSender.class.getName());

K
kohsuke 已提交
305 306
    public static boolean debug = false;

307 308
    private static final int MAX_LOG_LINES = 250;
}