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.pop3s.port", "995");
58
        properties.put("mail.store.protocol", "pop3");
59 1 1. getProperties : replaced return value with null for de/aikiit/mailclena/mail/MailClient::getProperties → SURVIVED
        return properties;
60
    }
61
62
    /**
63
     * Opens a mail folder in the given mode.
64
     *
65
     * @param mode see @{@link Folder#open(int)} for available options.
66
     * @return pair of @{@link Store} and @{@link Folder} if available.
67
     * @throws MessagingException if folder cannot be opened or store is inaccessible.
68
     */
69
    @VisibleForTesting
70
    Optional<Pair<Store, Folder>> openFolder(int mode) throws MessagingException {
71
        Session emailSession = Session.getDefaultInstance(getProperties());
72
        // emailSession.setDebug(true);
73
74
        Store store = emailSession.getStore(POP3S);
75 1 1. openFolder : removed call to jakarta/mail/Store::connect → SURVIVED
        store.connect(mailConfiguration.getHost(), mailConfiguration.getUsername(), mailConfiguration.getPassword());
76
77
        Folder emailFolder = store.getFolder(INBOX);
78 1 1. openFolder : removed call to jakarta/mail/Folder::open → NO_COVERAGE
        emailFolder.open(mode);
79 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));
80
    }
81
82
    /**
83
     * Shows a list of messages in the mailbox root folder. It accesses the folder in read-only mode.
84
     *
85
     * @return messages in given folder, -1 in case of errors.
86
     */
87
    // TODO show date of mails YYYYMMDD
88
    @VisibleForTesting
89
    long list() {
90
        try {
91
            Optional<Pair<Store, Folder>> folder = openFolder(Folder.READ_ONLY);
92
93 1 1. list : negated conditional → KILLED
            if (!folder.isPresent()) {
94
                log.error("Unable to open folder in read-only mode to list mails, will abort.");
95 1 1. list : replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
                return -1;
96
            }
97
98
            Pair<Store, Folder> storeAndFolder = folder.get();
99
            List<Message> messages = Arrays.asList(storeAndFolder.getRight().getMessages());
100
            final int size = messages.size();
101
102 1 1. list : negated conditional → KILLED
            if (size == 0) {
103
                log.info("No messages found - nothing to be done here.");
104
            } else {
105
106
                log.info("Found {} messages.", size);
107
108
                for (Message m : ProgressBar.wrap(messages, "Listing")) {
109
                    try {
110
                        log.info("{} bytes / {} / Message: {} / From: {}", m.getSize(), m.getSentDate(), m.getSubject(), Arrays.toString(m.getFrom()));
111
                    } catch (MessagingException e) {
112
                        log.error("Error while traversing messages", e);
113
                    }
114
                }
115
            }
116
117 1 1. list : removed call to jakarta/mail/Store::close → KILLED
            storeAndFolder.getLeft().close();
118 1 1. list : replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
            return size;
119
        } catch (MessagingException e) {
120
            log.error(e);
121
        }
122 1 1. list : replaced long return with 0 for de/aikiit/mailclena/mail/MailClient::list → SURVIVED
        return -1;
123
    }
124
125
    /**
126
     * Application option to delete existing messages.
127
     *
128
     * @return number of messages deleted, if any. Empty otherwise.
129
     */
130
    @VisibleForTesting
131
    Optional<Long> delete() {
132
        try {
133
            Optional<Pair<Store, Folder>> folder = openFolder(Folder.READ_WRITE);
134
135 1 1. delete : negated conditional → KILLED
            if (!folder.isPresent()) {
136
                log.error("Unable to open folder in write mode to remove mails, will abort.");
137
                return Optional.empty();
138
            }
139
140
            Pair<Store, Folder> storeAndFolder = folder.get();
141
            final Folder f = storeAndFolder.getRight();
142
            List<Message> messages = Arrays.asList(f.getMessages());
143
144
            final int count = messages.size();
145
            final AtomicLong mailSize = new AtomicLong(0L);
146 1 1. delete : negated conditional → KILLED
            if (count == 0) {
147
                log.info("Folder is empty already - nothing to be done here.");
148
            } else {
149
                log.info("Starting to delete {} messages.", count);
150
151
                for (Message message : ProgressBar.wrap(messages, "Deleting")) {
152
                    try {
153
                        long messageSize = message.getSize();
154
                        log.info("Marking for deletion {} bytes with subject: {}", messageSize, message.getSubject());
155 1 1. delete : removed call to jakarta/mail/Message::setFlag → KILLED
                        message.setFlag(Flags.Flag.DELETED, true);
156
                        mailSize.addAndGet(messageSize);
157
                    } catch (MessagingException e) {
158
                        log.error("Error while traversing messages for deletion", e);
159
                    }
160
                }
161
162 1 1. delete : removed call to jakarta/mail/Folder::close → KILLED
                f.close(true);
163
                log.info("Expunge folder to actually remove messages.");
164
                log.info("Finished to delete {} messages, set {} bytes free", count, mailSize.get());
165
            }
166 1 1. delete : removed call to jakarta/mail/Store::close → KILLED
            storeAndFolder.getLeft().close();
167
168 1 1. delete : replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient::delete → KILLED
            return Optional.of(mailSize.longValue());
169
        } catch (MessagingException e) {
170
            log.error(e);
171
        }
172
        return Optional.empty();
173
    }
174
175
    /**
176
     * Execute the given command or print an error message if the command is unknown.
177
     *
178
     * @param command command to execute, should be one of {@link MailClientCommands}.
179
     */
180
    public void execute(String command) {
181
        Optional<MailClientCommands> cmd = parse(command);
182 1 1. execute : negated conditional → KILLED
        if (!cmd.isPresent()) {
183
            log.info("No explicit command given, will fallback to 'list'");
184
            cmd = Optional.of(LIST);
185
        }
186
187
        switch (cmd.get()) {
188
            case CLEAN:
189
                long messages = list();
190 2 1. execute : changed conditional boundary → SURVIVED
2. execute : negated conditional → KILLED
                if (messages > 0) {
191
                    delete();
192
                }
193
                break;
194
            case LIST:
195
                list();
196
                break;
197
            default:
198
                log.error("If you see this message, please report a bug since the CLI parser has new commands.");
199
                // NOOP: avoid findbugs warning
200
                break;
201
        }
202
    }
203
204
    /**
205
     * Encapsulates available application commands for MailClena.
206
     */
207
    @VisibleForTesting
208
    enum MailClientCommands {
209
        /**
210
         * Option to list available mails.
211
         */
212
        LIST,
213
        /**
214
         * Option to purge existing mails.
215
         */
216
        CLEAN;
217
218
        /**
219
         * Tries to convert a given command into an available application command option.
220
         *
221
         * @param command alphanumeric representation of the command.
222
         * @return a valid application command or empty if invalid.
223
         */
224
        static Optional<MailClientCommands> parse(String command) {
225 1 1. parse : negated conditional → KILLED
            if (!Strings.isNullOrEmpty(command)) {
226
                String normalized = command.trim();
227
                for (MailClientCommands cmd : values()) {
228 1 1. parse : negated conditional → KILLED
                    if (normalized.equalsIgnoreCase(cmd.toString())) {
229 1 1. parse : replaced return value with Optional.empty for de/aikiit/mailclena/mail/MailClient$MailClientCommands::parse → KILLED
                        return Optional.of(cmd);
230
                    }
231
                }
232
            }
233
234
            return Optional.empty();
235
        }
236
237
    }
238
239
}

Mutations

59

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

75

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

78

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

79

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

93

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

95

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

102

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

117

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

118

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

122

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

135

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

146

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

155

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

162

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

166

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

168

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

182

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

190

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

225

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

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

229

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.20.0