2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mail.internal;
15 import static org.openhab.binding.mail.internal.MailBindingConstants.CHANNEL_TYPE_UID_FOLDER_MAILCOUNT;
16 import static org.openhab.binding.mail.internal.MailBindingConstants.CHANNEL_TYPE_UID_MAIL_CONTENT;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.util.Properties;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.stream.Collectors;
24 import java.util.stream.Stream;
26 import javax.mail.Address;
27 import javax.mail.Flags;
28 import javax.mail.Folder;
29 import javax.mail.Message;
30 import javax.mail.MessagingException;
31 import javax.mail.Session;
32 import javax.mail.Store;
33 import javax.mail.internet.MimeMessage;
34 import javax.mail.internet.MimeMultipart;
35 import javax.mail.search.FlagTerm;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.openhab.binding.mail.internal.config.POP3IMAPConfig;
40 import org.openhab.binding.mail.internal.config.POP3IMAPContentChannelConfig;
41 import org.openhab.binding.mail.internal.config.POP3IMAPMailCountChannelConfig;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.StringType;
44 import org.openhab.core.thing.Channel;
45 import org.openhab.core.thing.ChannelUID;
46 import org.openhab.core.thing.Thing;
47 import org.openhab.core.thing.ThingStatus;
48 import org.openhab.core.thing.ThingStatusDetail;
49 import org.openhab.core.thing.binding.BaseThingHandler;
50 import org.openhab.core.thing.binding.generic.ChannelTransformation;
51 import org.openhab.core.types.Command;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * The {@link POP3IMAPHandler} is responsible for handling commands, which are
57 * sent to one of the channels.
59 * @author Jan N. Klug - Initial contribution
62 public class POP3IMAPHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(POP3IMAPHandler.class);
65 private @NonNullByDefault({}) POP3IMAPConfig config;
66 private @Nullable ScheduledFuture<?> refreshTask;
67 private final String baseProtocol;
68 private String protocol = "imap";
70 public POP3IMAPHandler(Thing thing) {
72 baseProtocol = thing.getThingTypeUID().getId(); // pop3 or imap
76 public void handleCommand(ChannelUID channelUID, Command command) {
80 public void initialize() {
81 config = getConfigAs(POP3IMAPConfig.class);
83 protocol = baseProtocol;
85 if (config.security == ServerSecurity.SSL) {
86 protocol = protocol.concat("s");
89 if (config.port == 0) {
91 case "imap" -> config.port = 143;
92 case "imaps" -> config.port = 993;
93 case "pop3" -> config.port = 110;
94 case "pop3s" -> config.port = 995;
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
102 refreshTask = scheduler.scheduleWithFixedDelay(this::refresh, 0, config.refresh, TimeUnit.SECONDS);
103 updateStatus(ThingStatus.ONLINE);
106 @SuppressWarnings("null")
108 public void dispose() {
109 if (refreshTask != null) {
110 if (!refreshTask.isCancelled()) {
111 refreshTask.cancel(true);
116 private void refresh() {
117 if (Thread.currentThread().isInterrupted()) {
120 Properties props = new Properties();
121 props.setProperty("mail." + baseProtocol + ".starttls.enable", "true");
122 props.setProperty("mail.store.protocol", protocol);
123 Session session = Session.getInstance(props);
125 try (Store store = session.getStore()) {
126 store.connect(config.hostname, config.port, config.username, config.password);
128 for (Channel channel : thing.getChannels()) {
129 if (CHANNEL_TYPE_UID_FOLDER_MAILCOUNT.equals(channel.getChannelTypeUID())) {
130 final POP3IMAPMailCountChannelConfig channelConfig = channel.getConfiguration()
131 .as(POP3IMAPMailCountChannelConfig.class);
132 final String folderName = channelConfig.folder;
133 if (folderName == null || folderName.isEmpty()) {
134 logger.info("missing or empty folder name in channel {}", channel.getUID());
136 try (Folder mailbox = store.getFolder(folderName)) {
137 mailbox.open(Folder.READ_ONLY);
138 if (channelConfig.type == MailCountChannelType.TOTAL) {
139 updateState(channel.getUID(), new DecimalType(mailbox.getMessageCount()));
141 updateState(channel.getUID(), new DecimalType(
142 mailbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false)).length));
146 } else if (CHANNEL_TYPE_UID_MAIL_CONTENT.equals(channel.getChannelTypeUID())) {
147 final POP3IMAPContentChannelConfig channelConfig = channel.getConfiguration()
148 .as(POP3IMAPContentChannelConfig.class);
149 final String folderName = channelConfig.folder;
150 if (folderName == null || folderName.isEmpty()) {
151 logger.info("missing or empty folder name in channel '{}'", channel.getUID());
153 try (Folder mailbox = store.getFolder(folderName)) {
154 mailbox.open(channelConfig.markAsRead ? Folder.READ_WRITE : Folder.READ_ONLY);
155 Message[] messages = mailbox.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
156 for (Message message : messages) {
157 String subject = message.getSubject();
158 Address[] senders = message.getFrom();
159 String sender = senders == null ? ""
160 : Stream.of(senders).map(Address::toString).collect(Collectors.joining(","));
161 logger.debug("Processing `{}` from `{}`", subject, sender);
162 if (!channelConfig.subject.isBlank() && !subject.matches(channelConfig.subject)) {
163 logger.trace("Subject '{}' did not pass subject filter", subject);
166 if (!channelConfig.sender.isBlank() && !sender.matches(channelConfig.sender)) {
167 logger.trace("Sender '{}' did not pass filter '{}'", subject, channelConfig.sender);
170 Object rawContent = message.getContent();
171 String contentAsString;
172 if (rawContent instanceof String) {
173 logger.trace("Detected plain text message");
174 contentAsString = (String) rawContent;
175 } else if (rawContent instanceof MimeMessage mimeMessage) {
176 logger.trace("Detected MIME message");
177 try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
178 mimeMessage.writeTo(os);
179 contentAsString = os.toString();
181 } else if (rawContent instanceof MimeMultipart mimeMultipart) {
182 logger.trace("Detected MIME multipart message");
183 try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
184 mimeMultipart.writeTo(os);
185 contentAsString = os.toString();
189 "Failed to convert mail content from '{}' with subject '{}', to String: {}",
190 sender, subject, rawContent.getClass());
193 logger.trace("Found content '{}'", contentAsString);
194 new ChannelTransformation(channelConfig.transformation).apply(contentAsString)
195 .ifPresent(result -> updateState(channel.getUID(), new StringType(result)));
201 } catch (MessagingException | IOException e) {
202 logger.info("Failed refreshing IMAP for thing '{}': {}", thing.getUID(), e.getMessage());