diff --git a/bundles/org.smarthomej.binding.mail/pom.xml b/bundles/org.smarthomej.binding.mail/pom.xml
index 5a39ff25d4..849089c022 100644
--- a/bundles/org.smarthomej.binding.mail/pom.xml
+++ b/bundles/org.smarthomej.binding.mail/pom.xml
@@ -39,4 +39,26 @@
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+
+ add-source
+
+ generate-sources
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bundles/org.smarthomej.binding.mail/src/3rdparty/java/javax/activation/PatchedMailcapCommandMap.java b/bundles/org.smarthomej.binding.mail/src/3rdparty/java/javax/activation/PatchedMailcapCommandMap.java
new file mode 100644
index 0000000000..9b2f2041cd
--- /dev/null
+++ b/bundles/org.smarthomej.binding.mail/src/3rdparty/java/javax/activation/PatchedMailcapCommandMap.java
@@ -0,0 +1,433 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package javax.activation;
+
+import javax.mail.Session;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @version $Rev$ $Date$
+ *
+ * Changelog:
+ * - typed collections
+ * - use javax.mail.Session classloader instead of context classloader
+ * - remove unused constructors
+ * - code-style improvements
+ *
+ */
+public class PatchedMailcapCommandMap extends CommandMap {
+ /**
+ * A string that holds all the special chars.
+ */
+ private static final String TSPECIALS = "()<>@,;:/[]?=\\\"";
+ private final Map mimeTypes = new HashMap<>();
+ private final Map> preferredCommands = new HashMap<>();
+ private final Map> allCommands = new HashMap<>();
+ // the unparsed commands from the mailcap file.
+ private final Map> nativeCommands = new HashMap<>();
+ // commands identified as fallbacks...these are used last, and also used as wildcards.
+ private final Map> fallbackCommands = new HashMap<>();
+
+ public PatchedMailcapCommandMap() {
+ ClassLoader contextLoader = Objects.requireNonNull(Session.class.getClassLoader());
+ // process /META-INF/mailcap resources
+ try {
+ Enumeration e = contextLoader.getResources("META-INF/mailcap");
+ while (e.hasMoreElements()) {
+ URL url = e.nextElement();
+ try (InputStream is = url.openStream()) {
+ parseMailcap(is);
+ } catch (IOException ignored) {
+ }
+ }
+ } catch (SecurityException | IOException e) {
+ // ignore
+ }
+ }
+
+ private void parseMailcap(InputStream is) {
+ try {
+ parseMailcap(new InputStreamReader(is));
+ } catch (IOException e) {
+ // spec API means all we can do is swallow this
+ }
+ }
+
+ void parseMailcap(Reader reader) throws IOException {
+ BufferedReader br = new BufferedReader(reader);
+ String line;
+ while ((line = br.readLine()) != null) {
+ addMailcap(line);
+ }
+ }
+
+ public synchronized void addMailcap(String mail_cap) {
+ int index = 0;
+ // skip leading whitespace
+ index = skipSpace(mail_cap, index);
+ if (index == mail_cap.length() || mail_cap.charAt(index) == '#') {
+ return;
+ }
+
+ // get primary type
+ int start = index;
+ index = getToken(mail_cap, index);
+ if (start == index) {
+ return;
+ }
+ String mimeType = mail_cap.substring(start, index);
+
+ // skip any spaces after the primary type
+ index = skipSpace(mail_cap, index);
+ if (index == mail_cap.length() || mail_cap.charAt(index) == '#') {
+ return;
+ }
+
+ // get sub-type
+ if (mail_cap.charAt(index) == '/') {
+ index = skipSpace(mail_cap, ++index);
+ start = index;
+ index = getToken(mail_cap, index);
+ mimeType = mimeType + '/' + mail_cap.substring(start, index);
+ } else {
+
+ mimeType = mimeType + "/*";
+ }
+
+ // we record all mappings using the lowercase version.
+ mimeType = mimeType.toLowerCase();
+
+ // skip spaces after mime type
+ index = skipSpace(mail_cap, index);
+
+ // expect a ';' to terminate field 1
+ if (index == mail_cap.length() || mail_cap.charAt(index) != ';') {
+ return;
+ }
+ // ok, we've parsed the mime text field, now parse the view field. If there's something
+ // there, then we add this to the native text.
+ index = skipSpace(mail_cap, index + 1);
+ // if the next encountered text is not a ";", then we have a view. This gets added to the
+ // native list.
+ if (index == mail_cap.length() || mail_cap.charAt(index) != ';') {
+ List nativeCommandList = Objects.requireNonNull(nativeCommands.computeIfAbsent(mimeType, k -> new ArrayList<>()));
+ // now add this as an entry in the list.
+ nativeCommandList.add(mail_cap);
+ // now skip forward to the next field marker, if any
+ index = getMText(mail_cap, index);
+ }
+
+ // we don't know which list this will be added to until we finish parsing, as there
+ // can be an x-java-fallback-entry parameter that moves this to the fallback list.
+ List commandList = new ArrayList<>();
+ // but by default, this is not a fallback.
+ boolean fallback = false;
+
+ // parse fields
+ while (index < mail_cap.length() && mail_cap.charAt(index) == ';') {
+ index = skipSpace(mail_cap, index + 1);
+ start = index;
+ index = getToken(mail_cap, index);
+ String fieldName = mail_cap.substring(start, index).toLowerCase();
+ index = skipSpace(mail_cap, index);
+ if (index < mail_cap.length() && mail_cap.charAt(index) == '=') {
+ index = skipSpace(mail_cap, index + 1);
+ start = index;
+ index = getMText(mail_cap, index);
+ String value = mail_cap.substring(start, index);
+ index = skipSpace(mail_cap, index);
+ if (fieldName.startsWith("x-java-") && fieldName.length() > 7) {
+ String command = fieldName.substring(7);
+ value = value.trim();
+ if (command.equals("fallback-entry")) {
+ if (value.equals("true")) {
+ fallback = true;
+ }
+ }
+ else {
+ // create a CommandInfo item and add it the accumulator
+ CommandInfo info = new CommandInfo(command, value);
+ commandList.add(info);
+ }
+ }
+ }
+ }
+ addCommands(mimeType, commandList, fallback);
+ }
+
+ /**
+ * Add a parsed list of commands to the appropriate command list.
+ *
+ * @param mimeType The mimeType name this is added under.
+ * @param commands A List containing the command information.
+ * @param fallback The target list identifier.
+ */
+ protected void addCommands(String mimeType, List commands, boolean fallback) {
+ // add this to the mimeType set
+ mimeTypes.put(mimeType, mimeType);
+ // the target list changes based on the type of entry.
+ Map> target = fallback ? fallbackCommands : preferredCommands;
+
+ // now process
+ for (CommandInfo info : commands) {
+ addCommand(target, mimeType, info);
+ // if this is not a fallback position, then this to the allcommands list.
+ if (!fallback) {
+ List cmdList = allCommands.computeIfAbsent(mimeType, k -> new ArrayList<>());
+ addUnique(cmdList, info);
+ }
+ }
+ }
+
+ private void addUnique(List commands, CommandInfo newCommand) {
+ for (CommandInfo info : commands) {
+ if (info.getCommandName().equals(newCommand.getCommandName()) && info.getCommandClass()
+ .equals(newCommand.getCommandClass())) {
+ return;
+ }
+ }
+ commands.add(newCommand);
+ }
+
+ /**
+ * Add a command to a target command list (preferred or fallback).
+ *
+ * @param commandList
+ * The target command list.
+ * @param mimeType The MIME type the command is associated with.
+ * @param command The command information.
+ */
+ protected void addCommand(Map> commandList, String mimeType, CommandInfo command) {
+ Map commands = Objects.requireNonNull(commandList.computeIfAbsent(mimeType, k -> new HashMap<>()));
+ commands.put(command.getCommandName(), command);
+ }
+
+
+ private int skipSpace(String s, int index) {
+ while (index < s.length() && Character.isWhitespace(s.charAt(index))) {
+ index++;
+ }
+ return index;
+ }
+
+ private int getToken(String s, int index) {
+ while (index < s.length() && s.charAt(index) != '#' && !isSpecialCharacter(s.charAt(index))) {
+ index++;
+ }
+ return index;
+ }
+
+ private int getMText(String s, int index) {
+ while (index < s.length()) {
+ char c = s.charAt(index);
+ if (c == '#' || c == ';' || Character.isISOControl(c)) {
+ return index;
+ }
+ if (c == '\\') {
+ index++;
+ if (index == s.length()) {
+ return index;
+ }
+ }
+ index++;
+ }
+ return index;
+ }
+
+ public synchronized CommandInfo[] getPreferredCommands(String mimeType) {
+ // get the mimetype as a lowercase version.
+ mimeType = mimeType.toLowerCase();
+
+ Map commands = preferredCommands.get(mimeType);
+ if (commands == null) {
+ commands = preferredCommands.get(getWildcardMimeType(mimeType));
+ }
+
+ Map fallbackCommands = getFallbackCommands(mimeType);
+
+ // if we have fall backs, then we need to merge this stuff.
+ if (fallbackCommands != null) {
+ // if there's no command list, we can just use this as the master list.
+ if (commands == null) {
+ commands = fallbackCommands;
+ }
+ else {
+ // merge the two lists. The ones in the commands list will take precedence.
+ commands = mergeCommandMaps(commands, fallbackCommands);
+ }
+ }
+
+ // now convert this into an array result.
+ if (commands == null) {
+ return new CommandInfo[0];
+ }
+ return commands.values().toArray(new CommandInfo[0]);
+ }
+
+ private Map getFallbackCommands(String mimeType) {
+ Map commands = fallbackCommands.get(mimeType);
+
+ // now we also need to search this as if it was a wildcard. If we get a wildcard hit,
+ // we have to merge the two lists.
+ Map wildcardCommands = fallbackCommands.get(getWildcardMimeType(mimeType));
+ // no wildcard version
+ if (wildcardCommands == null) {
+ return commands;
+ }
+ // we need to merge these.
+ return mergeCommandMaps(commands, wildcardCommands);
+ }
+
+
+ private Map mergeCommandMaps(Map main, Map fallback) {
+ // create a cloned copy of the second map. We're going to use a PutAll operation to
+ // overwrite any duplicates.
+ Map result = new HashMap<>(fallback);
+ result.putAll(main);
+
+ return result;
+ }
+
+ public synchronized CommandInfo[] getAllCommands(String mimeType) {
+ mimeType = mimeType.toLowerCase();
+ List exactCommands = allCommands.get(mimeType);
+ if (exactCommands == null) {
+ exactCommands = List.of();
+ }
+ List wildCommands = allCommands.get(getWildcardMimeType(mimeType));
+ if (wildCommands == null) {
+ wildCommands = List.of();
+ }
+
+ Map fallbackCommands = getFallbackCommands(mimeType);
+ if (fallbackCommands == null) {
+ fallbackCommands = Map.of();
+ }
+
+
+ CommandInfo[] result = new CommandInfo[exactCommands.size() + wildCommands.size() + fallbackCommands.size()];
+ int j = 0;
+ for (CommandInfo exactCommand : exactCommands) {
+ result[j++] = exactCommand;
+ }
+ for (CommandInfo wildCommand : wildCommands) {
+ result[j++] = wildCommand;
+ }
+
+ for (String s : fallbackCommands.keySet()) {
+ result[j++] = fallbackCommands.get(s);
+ }
+ return result;
+ }
+
+ public synchronized CommandInfo getCommand(String mimeType, String cmdName) {
+ mimeType = mimeType.toLowerCase();
+ // strip any parameters from the supplied mimeType
+ int i = mimeType.indexOf(';');
+ if (i != -1) {
+ mimeType = mimeType.substring(0, i).trim();
+ }
+ cmdName = cmdName.toLowerCase();
+
+ // search for an exact match
+ Map commands = preferredCommands.get(mimeType);
+ if (commands == null || commands.get(cmdName) == null) {
+ // then a wild card match
+ commands = preferredCommands.get(getWildcardMimeType(mimeType));
+ if (commands == null || commands.get(cmdName) == null) {
+ // then fallback searches, both standard and wild card.
+ commands = fallbackCommands.get(mimeType);
+ if (commands == null || commands.get(cmdName) == null) {
+ commands = fallbackCommands.get(getWildcardMimeType(mimeType));
+ }
+ if (commands == null) {
+ return null;
+ }
+ }
+ }
+ return commands.get(cmdName);
+ }
+
+ private String getWildcardMimeType(String mimeType) {
+ int i = mimeType.indexOf('/');
+ if (i == -1) {
+ return mimeType + "/*";
+ } else {
+ return mimeType.substring(0, i + 1) + "*";
+ }
+ }
+
+ public synchronized DataContentHandler createDataContentHandler(String mimeType) {
+ CommandInfo info = getCommand(mimeType, "content-handler");
+ if (info == null) {
+ return null;
+ }
+
+ ClassLoader cl = Objects.requireNonNull(Session.class.getClassLoader());
+ try {
+ return (DataContentHandler) cl.loadClass(info.getCommandClass()).getDeclaredConstructor().newInstance();
+ } catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Get all MIME types known to this command map.
+ *
+ * @return A String array of the MIME type names.
+ */
+ public synchronized String[] getMimeTypes() {
+ List types = new ArrayList<>(mimeTypes.values());
+ return types.toArray(new String[0]);
+ }
+
+ /**
+ * Return the list of raw command strings parsed
+ * from the mailcap files for a given mimeType.
+ *
+ * @param mimeType The target mime type
+ *
+ * @return A String array of the raw command strings. Returns
+ * an empty array if the mimetype is not currently known.
+ */
+ public synchronized String[] getNativeCommands(String mimeType) {
+ List commands = nativeCommands.get(mimeType.toLowerCase());
+ if (commands == null) {
+ return new String[0];
+ }
+ return commands.toArray(new String[0]);
+ }
+
+ private boolean isSpecialCharacter(char c) {
+ return Character.isWhitespace(c) || Character.isISOControl(c) || TSPECIALS.indexOf(c) != -1;
+ }
+}
\ No newline at end of file
diff --git a/bundles/org.smarthomej.binding.mail/src/main/java/org/smarthomej/binding/mail/internal/SMTPHandler.java b/bundles/org.smarthomej.binding.mail/src/main/java/org/smarthomej/binding/mail/internal/SMTPHandler.java
index c79b839af1..4b0c1967e2 100644
--- a/bundles/org.smarthomej.binding.mail/src/main/java/org/smarthomej/binding/mail/internal/SMTPHandler.java
+++ b/bundles/org.smarthomej.binding.mail/src/main/java/org/smarthomej/binding/mail/internal/SMTPHandler.java
@@ -14,7 +14,10 @@
package org.smarthomej.binding.mail.internal;
import java.util.Collection;
-import java.util.Collections;
+import java.util.List;
+
+import javax.activation.PatchedMailcapCommandMap;
+import javax.mail.MessagingException;
import org.apache.commons.mail.DefaultAuthenticator;
import org.apache.commons.mail.Email;
@@ -40,6 +43,7 @@
@NonNullByDefault
public class SMTPHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(SMTPHandler.class);
+ private final PatchedMailcapCommandMap commandMap = new PatchedMailcapCommandMap();
private @NonNullByDefault({}) SMTPConfig config;
@@ -94,8 +98,11 @@ public boolean sendMail(Email mail) {
if (!config.username.isEmpty() && !config.password.isEmpty()) {
mail.setAuthenticator(new DefaultAuthenticator(config.username, config.password));
}
- mail.send();
- } catch (EmailException e) {
+
+ mail.buildMimeMessage();
+ mail.getMimeMessage().getDataHandler().setCommandMap(commandMap);
+ mail.sendMimeMessage();
+ } catch (MessagingException | EmailException e) {
Throwable cause = e.getCause();
if (cause != null) {
logger.warn("{}", cause.toString());
@@ -109,6 +116,6 @@ public boolean sendMail(Email mail) {
@Override
public Collection> getServices() {
- return Collections.singletonList(SendMailActions.class);
+ return List.of(SendMailActions.class);
}
}