From 54c89a960181ecdd847ff0fa252f57e5611dc0a8 Mon Sep 17 00:00:00 2001 From: Yurii Dubinka Date: Sun, 31 Mar 2019 22:31:10 +0530 Subject: [PATCH] #17: Download attachments from emails --- .circleci/config.yml | 4 +- .gitignore | 3 +- pom.xml | 10 +- .../mbox4j/inbox/javax/AttachmentOf.java | 90 ++++++++++++++++ .../dgroup/mbox4j/inbox/javax/PartsOf.java | 59 +++++++++++ .../mbox4j/inbox/javax/RecipientsOf.java | 63 +++++++++++ .../dgroup/mbox4j/inbox/javax/ToMsg.java | 100 +++++++++++++----- .../mbox4j/inbox/javax/search/mode/All.java | 25 ++++- .../dgroup/mbox4j/outbox/javax/MimeMsg.java | 8 +- .../inbox/javax/JavaxMailInboxTestIT.java | 28 ++++- 10 files changed, 343 insertions(+), 47 deletions(-) create mode 100644 src/main/java/io/github/dgroup/mbox4j/inbox/javax/AttachmentOf.java create mode 100644 src/main/java/io/github/dgroup/mbox4j/inbox/javax/PartsOf.java create mode 100644 src/main/java/io/github/dgroup/mbox4j/inbox/javax/RecipientsOf.java diff --git a/.circleci/config.yml b/.circleci/config.yml index c4ab4a3..c44e234 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,8 @@ # Environment variables: https://circleci.com/docs/2.0/env-vars # Verify circleci *.yml: https://circleci.com/docs/2.0/local-cli # +# @todo #/DEV Enable `qulice` plugin once https://github.com/teamed/qulice/issues/1035 is resolved. +# version: 2 jobs: assemble_jar: @@ -20,7 +22,7 @@ jobs: - run: name: Build java sources (including integration tests) command: | - mvn -X -P integration-tests,qulice clean install -DLL.yandex.user=${EMAIL_USER} -DLL.yandex.pass=${EMAIL_PASS} -DLL.yandex.to.user=${EMAIL_TO} + mvn -X -P integration-tests clean install -DLL.yandex.user=${EMAIL_USER} -DLL.yandex.pass=${EMAIL_PASS} -DLL.yandex.to.user=${EMAIL_TO} workflows: version: 2 diff --git a/.gitignore b/.gitignore index 92ad7a8..f3eaaf6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.iml *.iws *.class -/.idea/ \ No newline at end of file +/.idea/ +/.tmp/ \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0ef3b7c..d04e33f 100644 --- a/pom.xml +++ b/pom.xml @@ -31,9 +31,7 @@ 0.0.0 jar ${project.artifactId} - - Simplify manipulations with CLI terminal(s) for Java-based applications - + Simplify manipulations with CLI terminal(s) for Java-based applications http://github.com/dgroup/mbox4j @@ -61,9 +59,7 @@ ${project.version} MIT License - - https://github.com/dgroup/mbox4j/blob/master/license.txt - + https://github.com/dgroup/mbox4j/blob/master/license.txt 1.8 UTF-8 @@ -80,7 +76,7 @@ 1.5 1.0.0 0.7.9 - 0.18.9 + 0.18.13 3.4.0.905 1.2 diff --git a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/AttachmentOf.java b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/AttachmentOf.java new file mode 100644 index 0000000..08286bc --- /dev/null +++ b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/AttachmentOf.java @@ -0,0 +1,90 @@ +/* + * MIT License + * + * Copyright (c) 2019 Yurii Dubinka + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom + * the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.github.dgroup.mbox4j.inbox.javax; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import javax.mail.Part; +import org.cactoos.Scalar; +import org.cactoos.io.InputOf; +import org.cactoos.io.OutputTo; +import org.cactoos.io.TeeInput; +import org.cactoos.scalar.LengthOf; + +/** + * Represents an attachment from {@link javax.mail.Part}. + * + * @since 0.1.0 + */ +public final class AttachmentOf implements Scalar { + + /** + * An instance of {@link javax.mail.Part} with attachment. + */ + private final Part part; + + /** + * The temporal directory for the email attachments. + */ + private final Scalar tmp; + + /** + * Ctor. + * @param part An instance of {@link javax.mail.Part} with attachment. + * @param tmp The temporal directory for the email attachments. + */ + public AttachmentOf(final Part part, final Scalar tmp) { + this.part = part; + this.tmp = tmp; + } + + @Override + @SuppressWarnings("PMD.AvoidCatchingGenericException") + public File value() throws IOException { + try { + final Path dest = Paths.get( + this.tmp.value().getAbsolutePath(), this.part.getFileName() + ); + try (BufferedInputStream inp = new BufferedInputStream(this.part.getInputStream()); + BufferedOutputStream out = new BufferedOutputStream(Files.newOutputStream(dest))) { + new LengthOf( + new TeeInput( + new InputOf(inp), + new OutputTo(out) + ) + ).intValue(); + } + return dest.toFile(); + // @checkstyle IllegalCatchCheck (3 lines) + } catch (final Exception cause) { + throw new IOException(cause); + } + } +} diff --git a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/PartsOf.java b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/PartsOf.java new file mode 100644 index 0000000..1798467 --- /dev/null +++ b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/PartsOf.java @@ -0,0 +1,59 @@ +/* + * MIT License + * + * Copyright (c) 2019 Yurii Dubinka + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom + * the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.github.dgroup.mbox4j.inbox.javax; + +import java.util.ArrayList; +import java.util.Collection; +import javax.mail.Multipart; +import javax.mail.Part; +import org.cactoos.iterable.IterableEnvelope; + +/** + * Represents all parts of {@link javax.mail.Multipart} as an {@link Iterable}. + * + * @since 0.1.0 + * @todo #/DEV Add hierarchical unwrap as multipart can have tree-based structure. + * For example, the multipart can have few levels with multipart(s). + */ +public final class PartsOf extends IterableEnvelope { + + /** + * Ctor. + * @param multipart The content of email message. + */ + public PartsOf(final Multipart multipart) { + super( + () -> { + final int quantity = multipart.getCount(); + final Collection parts = new ArrayList<>(quantity); + for (int idx = 0; idx < quantity; ++idx) { + parts.add(multipart.getBodyPart(idx)); + } + return parts; + } + ); + } + +} diff --git a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/RecipientsOf.java b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/RecipientsOf.java new file mode 100644 index 0000000..e0a384d --- /dev/null +++ b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/RecipientsOf.java @@ -0,0 +1,63 @@ +/* + * MIT License + * + * Copyright (c) 2019 Yurii Dubinka + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom + * the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, + * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE + * OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package io.github.dgroup.mbox4j.inbox.javax; + +import java.util.Collections; +import java.util.Set; +import javax.mail.Address; +import javax.mail.Message; +import org.cactoos.collection.Mapped; +import org.cactoos.set.SetEnvelope; +import org.cactoos.set.SetOf; + +/** + * The email recipients based on their type for the dedicated message. + * + * @since 0.1.0 + */ +public final class RecipientsOf extends SetEnvelope { + + /** + * Ctor. + * @param type The type of recipients. + * @param msg The message with recipients. + */ + public RecipientsOf(final Message.RecipientType type, final Message msg) { + super( + () -> { + final Address[] addresses = msg.getRecipients(type); + final Set recipients; + if (addresses == null || addresses.length == 0) { + recipients = Collections.emptySet(); + } else { + recipients = new SetOf<>( + new Mapped<>(Address::toString, addresses) + ); + } + return recipients; + } + ); + } +} diff --git a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/ToMsg.java b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/ToMsg.java index c220fb3..b15ca6d 100644 --- a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/ToMsg.java +++ b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/ToMsg.java @@ -26,14 +26,15 @@ import io.github.dgroup.mbox4j.Msg; import io.github.dgroup.mbox4j.msg.MsgOf; -import java.util.Collections; -import java.util.Set; -import javax.mail.Address; +import java.io.File; +import java.util.Collection; +import java.util.LinkedList; import javax.mail.Message; import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.Part; import org.cactoos.Func; -import org.cactoos.collection.Mapped; -import org.cactoos.set.SetOf; +import org.cactoos.Scalar; /** * The function to map {@link javax.mail.Message} and {@link Msg}. @@ -43,43 +44,86 @@ * text-based thus we need to define a way how to send the files. * For now Collections.emptySet is used as a stub and should be * removed later. - * @todo #/DEV Message content: transform MimeMultipart to String - * For now the body content msg.getContent().toString() - * looks as ... body=javax.mail.internet.MimeMultipart@35a50a4c .... */ public final class ToMsg implements Func { + /** + * The temporal directory for the email attachments. + */ + private final Scalar tmp; + + /** + * Ctor. + */ + public ToMsg() { + this(() -> new File(".tmp")); + } + + /** + * Ctor. + * @param tmp The temporal directory for the email attachments. + */ + public ToMsg(final Scalar tmp) { + this.tmp = tmp; + } + @Override public Msg apply(final Message msg) throws Exception { + this.createTemporalDirectoryIfAbsent(); + final Object content = msg.getContent(); + final StringBuilder body = new StringBuilder(); + final Collection attachments = new LinkedList<>(); + if (textPlain(msg)) { + body.append(content.toString()); + } else if (content instanceof Multipart) { + final Multipart multipart = (Multipart) content; + for (final Part part : new PartsOf(multipart)) { + if (textPlain(part)) { + body.append(part.getContent().toString()); + } else if (Part.ATTACHMENT.equals(part.getDisposition())) { + attachments.add(new AttachmentOf(part, this.tmp).value()); + } + } + } return new MsgOf( msg.getFrom()[0].toString(), - recipients(Message.RecipientType.TO, msg), - recipients(Message.RecipientType.CC, msg), - recipients(Message.RecipientType.BCC, msg), + new RecipientsOf(Message.RecipientType.TO, msg), + new RecipientsOf(Message.RecipientType.CC, msg), + new RecipientsOf(Message.RecipientType.BCC, msg), msg.getSubject(), - msg.getContent().toString(), - Collections.emptySet() + body.toString(), + attachments ); } /** - * Fetch the recipients based on their type from the message. - * @param type The type of recipients. - * @param msg The message with recipients. - * @return The recipients. - * @throws MessagingException In case if recipients can't be retrieved. + * Create the temporal directory for the attachments from the email. + * @throws Exception In case if temporal directory can't be detected + * or created. */ - private static Set recipients(final Message.RecipientType type, final Message msg) - throws MessagingException { - final Address[] addresses = msg.getRecipients(type); - final Set recipients; - if (addresses == null || addresses.length == 0) { - recipients = Collections.emptySet(); - } else { - recipients = new SetOf<>( - new Mapped<>(Address::toString, addresses) + private void createTemporalDirectoryIfAbsent() throws Exception { + final File ftmp = this.tmp.value(); + if (ftmp.exists() && ftmp.isDirectory()) { + return; + } + if (!ftmp.exists() && !ftmp.mkdir()) { + throw new IllegalArgumentException( + String.format( + "Can't initiate the '%s' as temporal directory for email storing", + ftmp.getAbsolutePath() + ) ); } - return recipients; + } + + /** + * Ensure that {@link javax.mail.Part} has content `text/plain`. + * @param part The original part. + * @return The true when type is matched. + * @throws MessagingException In the case of connectivity issues. + * @see Part#getContentType() + */ + private static boolean textPlain(final Part part) throws MessagingException { + return part.getContentType().contains("text/plain"); } } diff --git a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/search/mode/All.java b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/search/mode/All.java index f8fd728..ac6b620 100644 --- a/src/main/java/io/github/dgroup/mbox4j/inbox/javax/search/mode/All.java +++ b/src/main/java/io/github/dgroup/mbox4j/inbox/javax/search/mode/All.java @@ -28,6 +28,7 @@ import io.github.dgroup.mbox4j.inbox.javax.ToMsg; import java.util.ArrayList; import javax.mail.Folder; +import javax.mail.Message; import org.cactoos.Func; import org.cactoos.collection.Mapped; @@ -39,10 +40,28 @@ */ public final class All implements Func> { + /** + * The function to map {@link javax.mail.Message} to {@link Msg}. + */ + private final Func fnc; + + /** + * Ctor. + */ + public All() { + this(new ToMsg()); + } + + /** + * Ctor. + * @param fnc The function to map {@link javax.mail.Message} to {@link Msg}. + */ + public All(final Func fnc) { + this.fnc = fnc; + } + @Override public Iterable apply(final Folder folder) throws Exception { - return new ArrayList<>( - new Mapped<>(new ToMsg(), folder.getMessages()) - ); + return new ArrayList<>(new Mapped<>(this.fnc, folder.getMessages())); } } diff --git a/src/main/java/io/github/dgroup/mbox4j/outbox/javax/MimeMsg.java b/src/main/java/io/github/dgroup/mbox4j/outbox/javax/MimeMsg.java index 5b66bc2..a189c0a 100644 --- a/src/main/java/io/github/dgroup/mbox4j/outbox/javax/MimeMsg.java +++ b/src/main/java/io/github/dgroup/mbox4j/outbox/javax/MimeMsg.java @@ -54,9 +54,13 @@ public Message apply(final Session session, final Msg msg) throws Exception { email.setRecipients(Message.RecipientType.CC, new Addresses(msg.cc()).value()); email.setRecipients(Message.RecipientType.BCC, new Addresses(msg.bcc()).value()); email.setSubject(msg.subject().asString()); - email.setText(msg.body().asString()); - if (!msg.attachments().isEmpty()) { + if (msg.attachments().isEmpty()) { + email.setText(msg.body().asString()); + } else { final Multipart multipart = new MimeMultipart(); + final MimeBodyPart text = new MimeBodyPart(); + text.setText(msg.body().asString()); + multipart.addBodyPart(text); for (final File attachment : msg.attachments()) { final MimeBodyPart part = new MimeBodyPart(); final DataSource source = new FileDataSource(attachment); diff --git a/src/test/java/io/github/dgroup/mbox4j/inbox/javax/JavaxMailInboxTestIT.java b/src/test/java/io/github/dgroup/mbox4j/inbox/javax/JavaxMailInboxTestIT.java index 03d4a60..4111de7 100644 --- a/src/test/java/io/github/dgroup/mbox4j/inbox/javax/JavaxMailInboxTestIT.java +++ b/src/test/java/io/github/dgroup/mbox4j/inbox/javax/JavaxMailInboxTestIT.java @@ -32,9 +32,11 @@ import io.github.dgroup.mbox4j.query.QueryOf; import io.github.dgroup.mbox4j.query.mode.Mode; import io.github.dgroup.mbox4j.query.mode.ModeOf; +import java.io.File; import java.util.ArrayList; import javax.mail.Folder; import org.cactoos.Func; +import org.cactoos.collection.Joined; import org.cactoos.collection.Mapped; import org.cactoos.map.MapEntry; import org.cactoos.map.MapOf; @@ -58,7 +60,7 @@ public final class JavaxMailInboxTestIT { public void size() { new Assertion<>( "3 emails were read", - () -> this.read(3), + () -> this.read(1, 3), Matchers.iterableWithSize(3) ).affirm(); } @@ -69,7 +71,7 @@ public void subject() { "3 emails have expected subject", () -> new Mapped<>( msg -> msg.subject().asString(), - this.read(3) + this.read(1, 3) ), new HasValues<>( "The first email", @@ -79,18 +81,33 @@ public void subject() { ).affirm(); } + @Test + public void attachments() { + new Assertion<>( + "2 file names from email has expected names", + () -> new Mapped<>( + File::getName, + new Joined<>( + new Mapped<>(Msg::attachments, this.read(4, 4)) + ) + ), + new HasValues<>(".gitignore", ".pdd") + ).affirm(); + } + /** * Read range of emails from the dedicated SMTP server. - * @param quantity The quantity of messages to be fetched from SMTP server. + * @param start + * @param end * @return The emails. * @throws EmailException In the case of connectivity issues. */ - private Iterable read(final int quantity) throws EmailException { + private Iterable read(final int start, final int end) throws EmailException { final Mode mode = new ModeOf("first several emails"); final Query query = new QueryOf("imaps", "INBOX", mode); final Func> fnc = folder -> new ArrayList<>( new Mapped<>( - new ToMsg(), folder.getMessages(1, quantity) + new ToMsg(), folder.getMessages(start, end) ) ); return new JavaxMailInbox( @@ -98,4 +115,5 @@ private Iterable read(final int quantity) throws EmailException { new Modes(new MapOf<>(new MapEntry<>(mode, fnc))) ).read(query); } + }