OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 67 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
17 ilm 1
/*
2
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
3
 *
4
 * Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
5
 *
6
 * The contents of this file are subject to the terms of the GNU General Public License Version 3
7
 * only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
8
 * copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing the software, include this License Header Notice in each file.
12
 */
13
 
14
 package org.openconcerto.utils;
15
 
16
import org.openconcerto.utils.DesktopEnvironment.Gnome;
17
import org.openconcerto.utils.DesktopEnvironment.KDE;
18
import org.openconcerto.utils.DesktopEnvironment.Mac;
19
import org.openconcerto.utils.DesktopEnvironment.Windows;
20
import org.openconcerto.utils.io.PercentEncoder;
21
 
22
import java.io.BufferedOutputStream;
23
import java.io.BufferedWriter;
24
import java.io.File;
25
import java.io.IOException;
26
import java.io.OutputStreamWriter;
27
import java.io.PrintStream;
28
import java.io.Writer;
29
import java.net.URI;
30
import java.net.URISyntaxException;
31
import java.util.ArrayList;
32
import java.util.Arrays;
33
import java.util.List;
34
import java.util.regex.Matcher;
35
import java.util.regex.Pattern;
36
 
37
public abstract class EmailClient {
38
 
39
    public static enum EmailClientType {
40
        Thunderbird, AppleMail, Outlook
41
    }
42
 
43
    private static EmailClient PREFERRED = null;
44
 
45
    /**
46
     * Find the preferred email client.
47
     *
48
     * @return the preferred email client, never <code>null</code>.
49
     * @throws IOException if an error occurs.
50
     */
51
    public static final EmailClient getPreferred() throws IOException {
52
        if (PREFERRED == null) {
53
            PREFERRED = findPreferred();
54
            // should at least return MailTo
55
            assert PREFERRED != null;
56
        }
57
        return PREFERRED;
58
    }
59
 
60
    /**
61
     * Clear the preferred client.
62
     */
63
    public static final void resetPreferred() {
64
        PREFERRED = null;
65
    }
66
 
67
    // XP used tabs, but not 7
68
    // MULTILINE since there's several lines in addition to the wanted one
69
    private static final Pattern registryPattern = Pattern.compile("\\s+REG_SZ\\s+(.*)$", Pattern.MULTILINE);
70
    private static final Pattern cmdLinePattern = Pattern.compile("(\"(.*?)\")|([^\\s\"]+)\\b");
28 ilm 71
    // any whitespace except space and tab
72
    private static final Pattern wsPattern = Pattern.compile("[\\s&&[^ \t]]");
17 ilm 73
    private static final Pattern dictPattern;
74
    private static final String AppleMailBundleID = "com.apple.mail";
75
    private static final String ThunderbirdBundleID = "org.mozilla.thunderbird";
76
    static {
77
        final String rolePattern = "(?:LSHandlerRoleAll\\s*=\\s*\"([\\w\\.]+)\";\\s*)?";
78
        dictPattern = Pattern.compile("\\{\\s*" + rolePattern + "LSHandlerURLScheme = mailto;\\s*" + rolePattern + "\\}");
79
    }
80
 
28 ilm 81
    private final static String createEncodedParam(final String name, final String value) {
82
        return name + "=" + PercentEncoder.encode(value, StringUtils.UTF8);
17 ilm 83
    }
84
 
85
    private final static String createASParam(final String name, final String value) {
28 ilm 86
        return name + ":" + StringUtils.doubleQuote(value);
17 ilm 87
    }
88
 
89
    private final static String createVBParam(final String name, final String value) {
90
        final String switchName = "/" + name + ":";
91
        if (value == null || value.length() == 0)
92
            return switchName;
93
        // we need to encode the value since when invoking cscript.exe we cannot pass "
94
        // since all arguments are re-parsed
95
        final String encoded = PercentEncoder.encodeUTF16(value);
96
        assert encoded.indexOf('"') < 0 : "Encoded contains a double quote, this will confuse cscript";
97
        return switchName + '"' + encoded + '"';
98
    }
99
 
100
    /**
101
     * Create a mailto URI.
102
     *
103
     * @param to the recipient, can be <code>null</code>.
104
     * @param subject the subject, can be <code>null</code>.
105
     * @param body the body of the email, can be <code>null</code>.
28 ilm 106
     * @param attachments files to attach, for security reason this parameter is ignored by at least
107
     *        Outlook 2007, Apple Mail and Thunderbird.
17 ilm 108
     * @return the mailto URI.
109
     * @throws IOException if an encoding error happens.
110
     * @see <a href="http://tools.ietf.org/html/rfc2368">RFC 2368</a>
61 ilm 111
     * @see <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=67254">Don&apos;t allow attachment
112
     *      of local file from non-local link</a>
17 ilm 113
     */
28 ilm 114
    public final static URI getMailToURI(final String to, final String subject, final String body, final File... attachments) throws IOException {
17 ilm 115
        // mailto:p.dupond@example.com?subject=Sujet%20du%20courrier&cc=pierre@example.org&bcc=jacques@example.net&body=Bonjour
116
 
117
        // Outlook doesn't support the to header as mandated by 2. of the RFC
28 ilm 118
        final String encodedTo = to == null ? "" : PercentEncoder.encode(to, StringUtils.UTF8);
17 ilm 119
        final List<String> l = new ArrayList<String>(4);
120
        if (subject != null)
121
            l.add(createEncodedParam("subject", subject));
122
        if (body != null)
123
            l.add(createEncodedParam("body", body));
28 ilm 124
        for (final File attachment : attachments)
17 ilm 125
            l.add(createEncodedParam("attachment", attachment.getAbsolutePath()));
126
        final String query = CollectionUtils.join(l, "&");
127
        try {
128
            return new URI("mailto:" + encodedTo + "?" + query);
129
        } catch (URISyntaxException e) {
130
            throw new IOException("Couldn't create mailto URI", e);
131
        }
132
    }
133
 
28 ilm 134
    // see http://kb.mozillazine.org/Command_line_arguments_(Thunderbird)
135
    // The escape mechanism isn't specified, it turns out we can pass percent encoded strings
136
    private final static String getTBParam(final String to, final String subject, final String body, final File... attachments) {
137
        // "to='john@example.com,kathy@example.com',cc='britney@example.com',subject='dinner',body='How about dinner tonight?',attachment='file:///C:/cygwin/Cygwin.bat,file:///C:/cygwin/Cygwin.ico'";
17 ilm 138
 
139
        final List<String> l = new ArrayList<String>(4);
140
        if (to != null)
28 ilm 141
            l.add(createEncodedParam("to", to));
17 ilm 142
        if (subject != null)
28 ilm 143
            l.add(createEncodedParam("subject", subject));
17 ilm 144
        if (body != null)
28 ilm 145
            l.add(createEncodedParam("body", body));
146
        final List<String> urls = new ArrayList<String>(attachments.length);
147
        for (final File attachment : attachments) {
17 ilm 148
            // Thunderbird doesn't parse java URI file:/C:/
61 ilm 149
            final String rawPath = attachment.toURI().getRawPath();
150
            // handle UNC paths
151
            final String tbURL = (rawPath.startsWith("//") ? "file:///" : "file://") + rawPath;
28 ilm 152
            urls.add(tbURL);
17 ilm 153
        }
28 ilm 154
        l.add(createEncodedParam("attachment", CollectionUtils.join(urls, ",")));
17 ilm 155
 
28 ilm 156
        return DesktopEnvironment.getDE().quoteParamForExec(CollectionUtils.join(l, ","));
17 ilm 157
    }
158
 
159
    private final static String getAppleMailParam(final String subject, final String body) {
160
        final List<String> l = new ArrayList<String>(3);
161
        l.add("visible:true");
162
        if (subject != null)
163
            l.add(createASParam("subject", subject));
164
        if (body != null)
165
            l.add(createASParam("content", body));
166
 
167
        return CollectionUtils.join(l, ", ");
168
    }
169
 
170
    // @param cmdLine "C:\Program Files\Mozilla Thunderbird\thunderbird.exe" -osint -compose "%1"
171
    // @param toReplace "%1"
28 ilm 172
    private static String[] tbCommand(final String cmdLine, final String toReplace, final String to, final String subject, final String body, final File... attachments) {
173
        final String composeArg = getTBParam(to, subject, body, attachments);
17 ilm 174
 
175
        final List<String> arguments = new ArrayList<String>();
176
        final Matcher cmdMatcher = cmdLinePattern.matcher(cmdLine);
177
        while (cmdMatcher.find()) {
178
            final String quoted = cmdMatcher.group(2);
179
            final String unquoted = cmdMatcher.group(3);
180
            assert quoted == null ^ unquoted == null : "Both quoted and unquoted, or neither quoted nor quoted: " + quoted + " and " + unquoted;
181
            final String arg = quoted != null ? quoted : unquoted;
182
 
183
            final boolean replace = arg.equals(toReplace);
184
            // e.g. on Linux
185
            if (replace && !arguments.contains("-compose"))
186
                arguments.add("-compose");
187
            arguments.add(replace ? composeArg : arg);
188
        }
189
 
190
        return arguments.toArray(new String[arguments.size()]);
191
    }
192
 
193
    /**
194
     * Open a composing window in the default email client.
195
     *
196
     * @param to the recipient, can be <code>null</code>.
197
     * @param subject the subject, can be <code>null</code>.
198
     * @param body the body of the email, can be <code>null</code>.
28 ilm 199
     * @param attachments files to attach, ATTN can be ignored if mailto: is used
200
     *        {@link #getMailToURI(String, String, String, File...)}.
17 ilm 201
     * @throws IOException if a program cannot be executed.
202
     * @throws InterruptedException if the thread is interrupted while waiting for a native program.
203
     */
28 ilm 204
    public void compose(final String to, String subject, final String body, final File... attachments) throws IOException, InterruptedException {
17 ilm 205
        // check now as letting the native commands do is a lot less reliable
28 ilm 206
        for (File attachment : attachments) {
207
            if (!attachment.exists())
208
                throw new IOException("Attachment doesn't exist: '" + attachment.getAbsolutePath() + "'");
209
        }
17 ilm 210
 
28 ilm 211
        // a subject should only be one line (Thunderbird strips newlines anyway and Outlook sends a
212
        // malformed email)
213
        subject = wsPattern.matcher(subject).replaceAll(" ");
17 ilm 214
        final boolean handled;
215
        // was only trying native if necessary, but mailto url has length limitations and can have
216
        // encoding issues
28 ilm 217
        handled = composeNative(to, subject, body, attachments);
17 ilm 218
 
219
        if (!handled) {
28 ilm 220
            final URI mailto = getMailToURI(to, subject, body, attachments);
17 ilm 221
            java.awt.Desktop.getDesktop().mail(mailto);
222
        }
223
    }
224
 
225
    static private String cmdSubstitution(String... args) throws IOException {
226
        return DesktopEnvironment.cmdSubstitution(Runtime.getRuntime().exec(args));
227
    }
228
 
229
    private static EmailClient findPreferred() throws IOException {
230
        final DesktopEnvironment de = DesktopEnvironment.getDE();
231
        if (de instanceof Windows) {
232
            // Tested on XP and 7
233
            // <SANS NOM> REG_SZ "C:\Program Files\Mozilla
234
            // Thunderbird\thunderbird.exe" -osint -compose "%1"
235
            final String out = cmdSubstitution("reg", "query", "HKEY_CLASSES_ROOT\\mailto\\shell\\open\\command");
236
 
237
            final Matcher registryMatcher = registryPattern.matcher(out);
238
            if (registryMatcher.find()) {
239
                final String cmdLine = registryMatcher.group(1);
240
                if (cmdLine.contains("thunderbird")) {
241
                    return new ThunderbirdCommandLine(cmdLine, "%1");
242
                } else if (cmdLine.toLowerCase().contains("outlook")) {
243
                    return Outlook;
244
                }
245
            }
246
        } else if (de instanceof Mac) {
247
            // (
248
            // {
249
            // LSHandlerRoleAll = "com.apple.mail";
250
            // LSHandlerURLScheme = mailto;
251
            // }
252
            // )
253
            final String bundleID;
254
            final String dict = cmdSubstitution("defaults", "read", "com.apple.LaunchServices", "LSHandlers");
255
            final Matcher dictMatcher = dictPattern.matcher(dict);
256
            if (dictMatcher.find()) {
257
                // LSHandlerRoleAll can be before or after LSHandlerURLScheme
258
                final String before = dictMatcher.group(1);
259
                final String after = dictMatcher.group(2);
260
                assert before == null ^ after == null : "Both before and after, or neither before nor after: " + before + " and " + after;
261
                bundleID = before != null ? before : after;
262
            } else
263
                // the default
264
                bundleID = AppleMailBundleID;
265
 
266
            if (bundleID.equals(AppleMailBundleID)) {
267
                return AppleMail;
268
            } else if (bundleID.equals(ThunderbirdBundleID)) {
269
                // doesn't work if Thunderbird is already open:
270
                // https://bugzilla.mozilla.org/show_bug.cgi?id=424155
271
                // https://bugzilla.mozilla.org/show_bug.cgi?id=472891
272
                // MAYBE find out if launched and let handled=false
28 ilm 273
 
61 ilm 274
                final File appDir = ((Mac) de).getAppDir(bundleID);
17 ilm 275
                final File exe = new File(appDir, "Contents/MacOS/thunderbird-bin");
276
 
277
                return new ThunderbirdPath(exe);
278
            }
279
        } else if (de instanceof Gnome) {
280
            // evolution %s
281
            final String cmdLine = cmdSubstitution("gconftool", "-g", "/desktop/gnome/url-handlers/mailto/command");
282
            if (cmdLine.contains("thunderbird")) {
283
                return new ThunderbirdCommandLine(cmdLine, "%s");
284
            }
285
        } else if (de instanceof KDE) {
286
            // TODO look for EmailClient=/usr/bin/thunderbird in
287
            // ~/.kde/share/config/emaildefaults or /etc/kde (ou /usr/share/config qui est un
288
            // lien symbolique vers /etc/kde)
289
        }
290
 
291
        return MailTo;
292
    }
293
 
294
    public static final EmailClient MailTo = new EmailClient(null) {
295
        @Override
28 ilm 296
        public boolean composeNative(String to, String subject, String body, File... attachments) {
17 ilm 297
            return false;
298
        }
299
    };
300
 
301
    public static final EmailClient Outlook = new EmailClient(EmailClientType.Outlook) {
302
        @Override
28 ilm 303
        protected boolean composeNative(String to, String subject, String body, File... attachments) throws IOException, InterruptedException {
304
            final DesktopEnvironment de = DesktopEnvironment.getDE();
17 ilm 305
            final File vbs = FileUtils.getFile(EmailClient.class.getResource("OutlookEmail.vbs"));
306
            final List<String> l = new ArrayList<String>(6);
307
            l.add("cscript");
28 ilm 308
            l.add(de.quoteParamForExec(vbs.getAbsolutePath()));
17 ilm 309
            if (to != null)
310
                l.add(createVBParam("to", to));
311
            if (subject != null)
312
                l.add(createVBParam("subject", subject));
313
            // at least set a parameter otherwise the usage get displayed
314
            l.add(createVBParam("unicodeStdIn", "1"));
28 ilm 315
            for (File attachment : attachments) {
316
                l.add(de.quoteParamForExec(attachment.getAbsolutePath()));
317
            }
17 ilm 318
 
319
            final Process process = new ProcessBuilder(l).start();
320
            // VBScript only knows ASCII and UTF-16
28 ilm 321
            final Writer writer = new BufferedWriter(new OutputStreamWriter(process.getOutputStream(), StringUtils.UTF16));
17 ilm 322
            writer.write(body);
323
            writer.close();
324
            final int returnCode = process.waitFor();
325
            if (returnCode != 0)
326
                throw new IllegalStateException("Non zero return code: " + returnCode);
327
            return true;
328
        }
329
    };
330
 
331
    public static final EmailClient AppleMail = new EmailClient(EmailClientType.AppleMail) {
332
        @Override
28 ilm 333
        protected boolean composeNative(String to, String subject, String body, File... attachments) throws IOException, InterruptedException {
17 ilm 334
            final Process process = Runtime.getRuntime().exec(new String[] { "osascript" });
335
            final PrintStream w = new PrintStream(new BufferedOutputStream(process.getOutputStream()));
336
            // use ID to handle application renaming (always a slight delay after a rename for
337
            // this to work, though)
338
            w.println("tell application id \"" + AppleMailBundleID + "\"");
339
            w.println(" set theMessage to make new outgoing message with properties {" + getAppleMailParam(subject, body) + "}");
340
            if (to != null)
28 ilm 341
                w.println(" tell theMessage to make new to recipient with properties {address:" + StringUtils.doubleQuote(to) + "}");
342
            for (File attachment : attachments) {
343
                w.println(" tell content of theMessage to make new attachment with properties {file name:" + StringUtils.doubleQuote(attachment.getAbsolutePath()) + "} at after last paragraph");
17 ilm 344
            }
345
            w.println("end tell");
346
            w.close();
67 ilm 347
            if (w.checkError())
348
                throw new IOException();
17 ilm 349
 
350
            final int returnCode = process.waitFor();
351
            if (returnCode != 0)
352
                throw new IllegalStateException("Non zero return code: " + returnCode);
353
            return true;
354
        }
355
    };
356
 
357
    public static abstract class Thunderbird extends EmailClient {
358
 
359
        public static Thunderbird createFromExe(final File exe) {
83 ilm 360
            if (exe == null)
361
                throw new NullPointerException();
362
            if (!exe.isFile())
363
                return null;
17 ilm 364
            return new ThunderbirdPath(exe);
365
        }
366
 
367
        public static Thunderbird createFromCommandLine(final String cmdLine, final String toReplace) {
368
            return new ThunderbirdCommandLine(cmdLine, toReplace);
369
        }
370
 
371
        protected Thunderbird() {
372
            super(EmailClientType.Thunderbird);
373
        }
374
    }
375
 
376
    private static final class ThunderbirdCommandLine extends Thunderbird {
377
 
378
        private final String cmdLine;
379
        private final String toReplace;
380
 
381
        private ThunderbirdCommandLine(final String cmdLine, final String toReplace) {
382
            this.cmdLine = cmdLine;
383
            this.toReplace = toReplace;
384
        }
385
 
386
        @Override
28 ilm 387
        protected boolean composeNative(String to, String subject, String body, File... attachments) throws IOException {
388
            Runtime.getRuntime().exec(tbCommand(this.cmdLine, this.toReplace, to, subject, body, attachments));
17 ilm 389
            // don't wait for Thunderbird to quit if it wasn't launched
390
            // (BTW return code of 1 means the program was already launched)
391
            return true;
392
        }
393
    }
394
 
395
    private static final class ThunderbirdPath extends Thunderbird {
396
 
397
        private final File exe;
398
 
399
        private ThunderbirdPath(File exe) {
400
            this.exe = exe;
401
        }
402
 
403
        @Override
28 ilm 404
        protected boolean composeNative(String to, String subject, String body, File... attachments) throws IOException {
405
            final String composeArg = getTBParam(to, subject, body, attachments);
17 ilm 406
            Runtime.getRuntime().exec(new String[] { this.exe.getPath(), "-compose", composeArg });
407
            return true;
408
        }
409
    }
410
 
411
    private final EmailClientType type;
412
 
413
    public EmailClient(EmailClientType type) {
414
        this.type = type;
415
    }
416
 
417
    public final EmailClientType getType() {
418
        return this.type;
419
    }
420
 
28 ilm 421
    protected abstract boolean composeNative(final String to, final String subject, final String body, final File... attachments) throws IOException, InterruptedException;
17 ilm 422
 
423
    public final static void main(String[] args) throws Exception {
424
        if (args.length == 1 && "--help".equals(args[0])) {
425
            System.out.println("Usage: java [-Dparam=value] " + EmailClient.class.getName() + " [EmailClientType args]");
426
            System.out.println("\tEmailClientType: mailto or " + Arrays.asList(EmailClientType.values()));
28 ilm 427
            System.out.println("\tparam: to, subject, body, files (seprated by ',' double it to escape)");
17 ilm 428
            return;
429
        }
430
 
431
        final EmailClient client = createFromString(args);
28 ilm 432
        final String to = System.getProperty("to", "Pierre Dupond <p.dupond@example.com>, p.dupont@server.com");
433
        // ',to=' to test escaping of Thunderbird (passing subject='foo'bar' works)
434
        final String subject = System.getProperty("subject", "Sujé € du courrier ',to='&;\\<> \"autre'\n2nd line");
435
        final String body = System.getProperty("body", "Bonjour,\n\tsingle ' double \" backslash(arrière) \\ slash /");
436
        final String filesPath = System.getProperty("files");
437
        final String[] paths = filesPath == null || filesPath.length() == 0 ? new String[0] : filesPath.split("(?<!,),(?!,)");
438
        final File[] f = new File[paths.length];
439
        for (int i = 0; i < f.length; i++) {
440
            f[i] = new File(paths[i].replace(",,", ","));
441
        }
17 ilm 442
        client.compose(to, subject, body, f);
443
    }
444
 
445
    private static final EmailClient createFromString(final String... args) throws IOException {
446
        // switch doesn't support null
447
        if (args.length == 0)
448
            return getPreferred();
449
        else if ("mailto".equals(args[0]))
450
            return MailTo;
451
 
452
        final EmailClientType t = EmailClientType.valueOf(args[0]);
453
        switch (t) {
454
        case Outlook:
455
            return Outlook;
456
        case AppleMail:
457
            return AppleMail;
458
        case Thunderbird:
83 ilm 459
            EmailClient res = null;
460
            if (args.length == 2) {
461
                final File exe = new File(args[1]);
462
                res = Thunderbird.createFromExe(exe);
463
                if (res == null)
464
                    throw new IOException("Invalid exe : " + exe);
465
            } else if (args.length == 3) {
466
                res = Thunderbird.createFromCommandLine(args[1], args[2]);
467
            } else {
17 ilm 468
                throw new IllegalArgumentException(t + " needs 1 or 2 arguments");
83 ilm 469
            }
470
            return res;
17 ilm 471
        default:
472
            throw new IllegalStateException("Unknown type " + t);
473
        }
474
    }
475
}