OpenConcerto

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

svn://code.openconcerto.org/openconcerto

Rev

Rev 149 | Rev 182 | Go to most recent revision | Only display areas with differences | Regard whitespace | Details | Blame | Last modification | View Log | RSS feed

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