MailClient.java

1
/*
2
  MailClena - Copyright (C) 2018, Aiki IT
3
  <p>
4
  This program is free software: you can redistribute it and/or modify
5
  it under the terms of the GNU General Public License as published by
6
  the Free Software Foundation, either version 3 of the License, or
7
  (at your option) any later version.
8
  <p>
9
  This program is distributed in the hope that it will be useful,
10
  but WITHOUT ANY WARRANTY; without even the implied warranty of
11
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
  GNU General Public License for more details.
13
  <p>
14
  You should have received a copy of the GNU General Public License
15
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
package de.aikiit.mailclena.mail;
18
19
import com.google.common.base.Strings;
20
import de.aikiit.mailclena.MailConfiguration;
21
import jakarta.mail.*;
22
import lombok.AccessLevel;
23
import lombok.AllArgsConstructor;
24
import lombok.NoArgsConstructor;
25
import lombok.extern.log4j.Log4j2;
26
import me.tongfei.progressbar.ProgressBar;
27
import org.apache.commons.lang3.tuple.Pair;
28
import org.assertj.core.util.VisibleForTesting;
29
30
import java.util.Arrays;
31
import java.util.List;
32
import java.util.Optional;
33
import java.util.Properties;
34
import java.util.concurrent.atomic.AtomicLong;
35
36
import static de.aikiit.mailclena.mail.MailClient.MailClientCommands.LIST;
37
import static de.aikiit.mailclena.mail.MailClient.MailClientCommands.parse;
38
39
/**
40
 * Encapsulates technical access to mail inbox based on the given application/mail configuration.
41
 */
42
@AllArgsConstructor
43
@Log4j2
44
@NoArgsConstructor(access = AccessLevel.PRIVATE)
45
public final class MailClient {
46
47
    private static final String INBOX = "INBOX";
48
    private static final String POP3S = "pop3s";
49
50
    @VisibleForTesting
51
    private MailConfiguration mailConfiguration;
52
53
    private Properties getProperties() {
54
        Properties properties = new Properties();
55
        properties.put("mail.pop3s.host", mailConfiguration.getHost());
56
        properties.put("mail.pop3.starttls.enable", "true");
57
        properties.put("mail.pop3.starttls.required", "true");
58
        properties.put("mail.pop3s.port", "995");
59
        properties.put("mail.store.protocol", "pop3");
60
        // in case of Goneo certificate errors: enable debug
61
        // properties.put("mail.debug", "true");
62 1 1. getProperties : replaced return value with null for de/aikiit/mailclena/mail/MailClient::getProperties → SURVIVED
        return properties;
63
    }
64
65
    /**
66
     * Opens a mail folder in the given mode.
67
     *
68
     * @param mode see @{@link Folder#open(int)} for available options.
69
     * @return pair of @{@link Store} and @{@link Folder} if available.
70
     * @throws MessagingException if folder cannot be opened or store is inaccessible.
71
     */
72
    @VisibleForTesting
73
    Optional<Pair<Store, Folder>> openFolder(int mode) throws MessagingException {
74
        Session emailSession = Session.getDefaultInstance(getProperties());
75
        // emailSession.setDebug(true);
76
77
        Store store = emailSession.getStore(POP3S);
78 1 1. openFolder : removed call to jakarta/mail/Store::connect → SURVIVED
        store.connect(mailConfiguration.getHost(), mailConfiguration.getUsername(), mailConfiguration.getPassword());
79
80
        Folder emailFolder = store.getFolder(INBOX);
81 1 1. openFolder : removed call to jakarta/mail/Folder::open → NO_COVERAGE
        emailFolder.open(mode);
82 1 1. openFolder : replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient::openFolder → NO_COVERAGE
        return Optional.of(Pair.of(store, emailFolder));
83
    }
84
85
    /**
86
     * Shows a list of messages in the mailbox root folder. It accesses the folder in read-only mode.
87
     *
88
     * @return messages in given folder, -1 in case of errors.
89
     */
90
    // TODO show date of mails YYYYMMDD
91
    @VisibleForTesting
92
    long list() {
93
        try {
94
            Optional<Pair<Store, Folder>> folder = openFolder(Folder.READ_ONLY);
95
96 1 1. list : negated conditional → KILLED
            if (!folder.isPresent()) {
97
                log.error("Unable to open folder in read-only mode to list mails, will abort.");
98 1 1. list : replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
                return -1;
99
            }
100
101
            Pair<Store, Folder> storeAndFolder = folder.get();
102
            List<Message> messages = Arrays.asList(storeAndFolder.getRight().getMessages());
103
            final int size = messages.size();
104
105 1 1. list : negated conditional → KILLED
            if (size == 0) {
106
                log.info("No messages found - nothing to be done here.");
107
            } else {
108
109
                log.info("Found {} messages.", size);
110
111
                for (Message m : ProgressBar.wrap(messages, "Listing")) {
112
                    try {
113
                        log.info("{} bytes / {} / Message: {} / From: {}", m.getSize(), m.getSentDate(), m.getSubject(), Arrays.toString(m.getFrom()));
114
                    } catch (MessagingException e) {
115
                        log.error("Error while traversing messages", e);
116
                    }
117
                }
118
            }
119
120 1 1. list : removed call to jakarta/mail/Store::close → KILLED
            storeAndFolder.getLeft().close();
121 1 1. list : replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
            return size;
122
        } catch (MessagingException e) {
123
            log.error(e);
124
        }
125 1 1. list : replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
        return -1;
126
    }
127
128
    /**
129
     * Application option to delete existing messages.
130
     *
131
     * @return number of messages deleted, if any. Empty otherwise.
132
     */
133
    @VisibleForTesting
134
    Optional<Long> delete() {
135
        try {
136
            Optional<Pair<Store, Folder>> folder = openFolder(Folder.READ_WRITE);
137
138 1 1. delete : negated conditional → KILLED
            if (!folder.isPresent()) {
139
                log.error("Unable to open folder in write mode to remove mails, will abort.");
140
                return Optional.empty();
141
            }
142
143
            Pair<Store, Folder> storeAndFolder = folder.get();
144
            final Folder f = storeAndFolder.getRight();
145
            List<Message> messages = Arrays.asList(f.getMessages());
146
147
            final int count = messages.size();
148
            final AtomicLong mailSize = new AtomicLong(0L);
149 1 1. delete : negated conditional → KILLED
            if (count == 0) {
150
                log.info("Folder is empty already - nothing to be done here.");
151
            } else {
152
                log.info("Starting to delete {} messages.", count);
153
154
                for (Message message : ProgressBar.wrap(messages, "Deleting")) {
155
                    try {
156
                        long messageSize = message.getSize();
157
                        log.info("Marking for deletion {} bytes with subject: {}", messageSize, message.getSubject());
158 1 1. delete : removed call to jakarta/mail/Message::setFlag → KILLED
                        message.setFlag(Flags.Flag.DELETED, true);
159
                        mailSize.addAndGet(messageSize);
160
                    } catch (MessagingException e) {
161
                        log.error("Error while traversing messages for deletion", e);
162
                    }
163
                }
164
165 1 1. delete : removed call to jakarta/mail/Folder::close → KILLED
                f.close(true);
166
                log.info("Expunge folder to actually remove messages.");
167
                log.info("Finished to delete {} messages, set {} bytes free", count, mailSize.get());
168
            }
169 1 1. delete : removed call to jakarta/mail/Store::close → KILLED
            storeAndFolder.getLeft().close();
170
171 1 1. delete : replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient::delete → KILLED
            return Optional.of(mailSize.longValue());
172
        } catch (MessagingException e) {
173
            log.error(e);
174
        }
175
        return Optional.empty();
176
    }
177
178
    /**
179
     * Execute the given command or print an error message if the command is unknown.
180
     *
181
     * @param command command to execute, should be one of {@link MailClientCommands}.
182
     */
183
    public void execute(String command) {
184
        Optional<MailClientCommands> cmd = parse(command);
185 1 1. execute : negated conditional → KILLED
        if (!cmd.isPresent()) {
186
            log.info("No explicit command given, will fallback to 'list'");
187
            cmd = Optional.of(LIST);
188
        }
189
190
        switch (cmd.get()) {
191
            case CLEAN:
192
                long messages = list();
193 2 1. execute : changed conditional boundary → SURVIVED
2. execute : negated conditional → KILLED
                if (messages > 0) {
194
                    delete();
195
                }
196
                break;
197
            case LIST:
198
                list();
199
                break;
200
            default:
201
                log.error("If you see this message, please report a bug since the CLI parser has new commands.");
202
                // NOOP: avoid findbugs warning
203
                break;
204
        }
205
    }
206
207
    /**
208
     * Encapsulates available application commands for MailClena.
209
     */
210
    @VisibleForTesting
211
    enum MailClientCommands {
212
        /**
213
         * Option to list available mails.
214
         */
215
        LIST,
216
        /**
217
         * Option to purge existing mails.
218
         */
219
        CLEAN;
220
221
        /**
222
         * Tries to convert a given command into an available application command option.
223
         *
224
         * @param command alphanumeric representation of the command.
225
         * @return a valid application command or empty if invalid.
226
         */
227
        static Optional<MailClientCommands> parse(String command) {
228 1 1. parse : negated conditional → KILLED
            if (!Strings.isNullOrEmpty(command)) {
229
                String normalized = command.trim();
230
                for (MailClientCommands cmd : values()) {
231 1 1. parse : negated conditional → KILLED
                    if (normalized.equalsIgnoreCase(cmd.toString())) {
232 1 1. parse : replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient$MailClientCommands::parse → KILLED
                        return Optional.of(cmd);
233
                    }
234
                }
235
            }
236
237
            return Optional.empty();
238
        }
239
240
    }
241
242
}

Mutations

62

1.1
Location : getProperties
Killed by : none
replaced return value with null for de/aikiit/mailclena/mail/MailClient::getProperties → SURVIVED
Covering tests

78

1.1
Location : openFolder
Killed by : none
removed call to jakarta/mail/Store::connect → SURVIVED
Covering tests

81

1.1
Location : openFolder
Killed by : none
removed call to jakarta/mail/Folder::open → NO_COVERAGE

82

1.1
Location : openFolder
Killed by : none
replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient::openFolder → NO_COVERAGE

96

1.1
Location : list
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:verifyListWorksExceptionlessWhenFolderCannotBeOpened()]
negated conditional → KILLED

98

1.1
Location : list
Killed by : none
replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
Covering tests

105

1.1
Location : list
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:verifyListingMessagesIsExceptionProof()]
negated conditional → KILLED

120

1.1
Location : list
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:verifyListingMessagesIsExceptionProof()]
removed call to jakarta/mail/Store::close → KILLED

121

1.1
Location : list
Killed by : none
replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
Covering tests

125

1.1
Location : list
Killed by : none
replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
Covering tests

138

1.1
Location : delete
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:verifyDeleteWorksExceptionlessWhenFolderCannotBeOpened()]
negated conditional → KILLED

149

1.1
Location : delete
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:deleteWithMockedMailInteractionAndNoMessages()]
negated conditional → KILLED

158

1.1
Location : delete
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:deleteWithMockedMailInteractionAndSizes()]
removed call to jakarta/mail/Message::setFlag → KILLED

165

1.1
Location : delete
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:deleteWithMockedMailInteractionAndSizes()]
removed call to jakarta/mail/Folder::close → KILLED

169

1.1
Location : delete
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:deleteWithMockedMailInteractionAndNoMessages()]
removed call to jakarta/mail/Store::close → KILLED

171

1.1
Location : delete
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:deleteWithMockedMailInteractionAndSizes()]
replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient::delete → KILLED

185

1.1
Location : execute
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:parseUnknownCommandAndChooseFallback()]
negated conditional → KILLED

193

1.1
Location : execute
Killed by : none
changed conditional boundary → SURVIVED
Covering tests

2.2
Location : execute
Killed by : de.aikiit.mailclena.mail.MailClientTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientTest]/[method:parseDelete()]
negated conditional → KILLED

228

1.1
Location : parse
Killed by : de.aikiit.mailclena.mail.MailClientCommandsTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientCommandsTest]/[method:parseWithUnknownValue()]
negated conditional → KILLED

231

1.1
Location : parse
Killed by : de.aikiit.mailclena.mail.MailClientCommandsTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientCommandsTest]/[method:parseWithUnknownValue()]
negated conditional → KILLED

232

1.1
Location : parse
Killed by : de.aikiit.mailclena.mail.MailClientCommandsTest.[engine:junit-jupiter]/[class:de.aikiit.mailclena.mail.MailClientCommandsTest]/[method:parseWithPossibleValuesIgnoringCasing()]
replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient$MailClientCommands::parse → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.0 support