001package de.deepamehta.plugins.mail;
002
003import java.io.File;
004import java.io.UnsupportedEncodingException;
005import java.util.Collection;
006import java.util.List;
007import java.util.Map;
008import java.util.logging.Level;
009import java.util.logging.Logger;
010import java.util.ArrayList;
011
012import javax.mail.internet.InternetAddress;
013import javax.mail.internet.AddressException;
014import javax.ws.rs.GET;
015import javax.ws.rs.POST;
016import javax.ws.rs.Path;
017import javax.ws.rs.PathParam;
018import javax.ws.rs.Produces;
019import javax.ws.rs.QueryParam;
020import javax.ws.rs.WebApplicationException;
021import javax.ws.rs.core.MediaType;
022
023import org.apache.commons.mail.EmailAttachment;
024import org.apache.commons.mail.EmailException;
025import org.apache.commons.mail.HtmlEmail;
026import org.codehaus.jettison.json.JSONException;
027import org.jsoup.nodes.Document;
028
029import de.deepamehta.core.Association;
030import de.deepamehta.core.ChildTopics;
031import de.deepamehta.core.RelatedTopic;
032import de.deepamehta.core.Topic;
033import de.deepamehta.core.model.AssociationModel;
034import de.deepamehta.core.model.ChildTopicsModel;
035import de.deepamehta.core.model.SimpleValue;
036import de.deepamehta.core.model.TopicModel;
037import de.deepamehta.core.model.TopicRoleModel;
038import de.deepamehta.core.osgi.PluginActivator;
039import de.deepamehta.core.service.Inject;
040import de.deepamehta.core.service.PluginService;
041import de.deepamehta.core.service.ResultList;
042import de.deepamehta.core.service.event.PostCreateTopicListener;
043import de.deepamehta.core.storage.spi.DeepaMehtaTransaction;
044import de.deepamehta.plugins.accesscontrol.model.ACLEntry;
045import de.deepamehta.plugins.accesscontrol.model.AccessControlList;
046import de.deepamehta.plugins.accesscontrol.model.Operation;
047import de.deepamehta.plugins.accesscontrol.model.UserRole;
048import de.deepamehta.plugins.accesscontrol.service.AccessControlService;
049import de.deepamehta.plugins.files.ResourceInfo;
050import de.deepamehta.plugins.files.service.FilesService;
051import de.deepamehta.plugins.mail.service.MailService;
052
053@Path("/mail")
054@Produces(MediaType.APPLICATION_JSON)
055public class MailPlugin extends PluginActivator implements MailService, PostCreateTopicListener {
056
057    private static Logger log = Logger.getLogger(MailPlugin.class.getName());
058
059    // URI constants
060
061    public static final String AGGREGATION = "dm4.core.aggregation";
062    public static final String COMPOSITION = "dm4.core.composition";
063    public static final String CHILD = "dm4.core.child";
064    public static final String CHILD_TYPE = "dm4.core.child_type";
065    public static final String TOPIC_TYPE = "dm4.core.topic_type";
066    public static final String PARENT = "dm4.core.parent";
067    public static final String PARENT_TYPE = "dm4.core.parent_type";
068    public static final String FILE = "dm4.files.file";
069    public static final String ATTACHMENTS = "attachments";
070    public static final String BODY = "dm4.mail.body";
071    public static final String EMAIL_ADDRESS = "dm4.contacts.email_address";
072    public static final String DATE = "dm4.mail.date";
073    public static final String FROM = "dm4.mail.from";
074    public static final String MAIL = "dm4.mail";
075    public static final String MESSAGE_ID = "dm4.mail.id";
076    public static final String RECIPIENT = "dm4.mail.recipient";
077    public static final String RECIPIENT_TYPE = "dm4.mail.recipient.type";
078    public static final String SENDER = "dm4.mail.sender";
079    public static final String SIGNATURE = "dm4.mail.signature";
080    public static final String SUBJECT = "dm4.mail.subject";
081    public static final String USER_ACCOUNT = "dm4.accesscontrol.user_account";
082
083    // service references
084    @Inject
085    private AccessControlService acService;
086    @Inject
087    private FilesService fileService = null;
088
089    // package internal helpers
090
091    MailConfigurationCache config = null;
092    ImageCidEmbedment cidEmbedment = null;
093    Autocomplete autocomplete = null;
094
095    boolean isInitialized;
096
097    /**
098     * @see #associateRecipient(long, long, RecipientType)
099     */
100    @POST
101    @Path("{mail}/recipient/{address}")
102    public Association associateRecipient(//
103            @PathParam("mail") long mailId,//
104            @PathParam("address") long addressId) {
105        return associateRecipient(mailId, addressId, config.getDefaultRecipientType());
106    }
107
108    @Override
109    public Association associateRecipient(long mailId, long addressId, RecipientType type) {
110        log.info("associate " + mailId + " with recipient address " + addressId);
111        // ### create value of the recipient association (#593 ref?)
112        ChildTopicsModel value = new ChildTopicsModel()//
113                .putRef(RECIPIENT_TYPE, type.getUri())//
114                .putRef(EMAIL_ADDRESS, addressId);
115        // get and update or create a new recipient association
116        RelatedTopic recipient = getContactOfEmail(addressId);
117        Association association = getRecipientAssociation(mailId, addressId, recipient.getId());
118        if (association == null) { // create a recipient association
119            return associateRecipient(mailId, recipient, value);
120        } else { // update address and type references
121            association.setChildTopics(value);
122            return association;
123        }
124    }
125
126    @Override
127    public void associateValidatedRecipients(long mailId, List<Topic> recipients) {
128        for (Topic recipient : recipients) {
129            Topic topic = dms.getTopic(recipient.getId()).loadChildTopics();
130            if (topic.getChildTopics().has(EMAIL_ADDRESS)) {
131                String personal = recipient.getSimpleValue().toString();
132                for (Topic email : topic.getChildTopics().getTopics(EMAIL_ADDRESS)) {
133                    String address = email.getSimpleValue().toString();
134                    try {
135                        new InternetAddress(address, personal).validate();
136                    } catch (Exception e) {
137                        log.log(Level.INFO, "email address '" + address + "' of contact '" + //
138                                personal + "'" + " is invalid: " + e.getMessage());
139                        continue; // check the next one
140                    }
141                    // associate validated email address as BCC recipient
142                    associateRecipient(mailId, email.getId(), RecipientType.BCC);
143                }
144            }
145        }
146    }
147
148    /**
149     * @see #associateSender(long, long)
150     */
151    @POST
152    @Path("{mail}/sender/{address}")
153    public Association mailSender(//
154            @PathParam("mail") long mailId,//
155            @PathParam("address") long addressId) {
156        return associateSender(mailId, addressId);
157    }
158
159    @Override
160    public Association associateSender(long mailId, long addressId) {
161        log.info("associate " + mailId + " with sender " + addressId);
162
163        // create value of sender association (#593 ref?)
164        ChildTopicsModel value = new ChildTopicsModel().putRef(EMAIL_ADDRESS, addressId);
165
166        // find existing sender association
167        RelatedTopic sender = getContactOfEmail(addressId);
168        RelatedTopic oldSender = getSender(mailId, false);
169
170        if (oldSender == null) { // create the first sender association
171            return associateSender(mailId, sender, value);
172        } else { // update or delete the old sender
173            Association association = getSenderAssociation(mailId, oldSender.getId());
174            DeepaMehtaTransaction tx = dms.beginTx();
175            try {
176                if (sender.getId() != oldSender.getId()) { // delete the old one
177                    dms.deleteAssociation(association.getId());
178                    association = associateSender(mailId, sender, value);
179                } else { // update composite
180                    association.setChildTopics(value);
181                }
182                tx.success();
183            } finally {
184                tx.finish();
185            }
186            return association;
187        }
188    }
189
190    /**
191     * Returns the parent of each search type substring match.
192     *
193     * @param term
194     *            String to search.
195     * @return Parent model of each result topic.
196     */
197    @GET
198    @Path("/autocomplete/{term}")
199    public List<TopicModel> search(@PathParam("term") String term) {
200        String query = "*" + term + "*";
201        log.info("autocomplete " + query);
202        return autocomplete.search(query);
203    }
204
205    /**
206     * Creates a copy of the mail.
207     *
208     * @param mailId
209     *            ID of the mail topic to clone.
210     * @param includeRecipients
211     *            Copy recipients of the origin?
212     * @return Cloned mail topic with associated sender and recipients.
213     */
214    @POST
215    @Path("/{mail}/copy")
216    public Topic copyMail(//
217            @PathParam("mail") long mailId,//
218            @QueryParam("recipients") boolean includeRecipients) {
219        log.info("copy mail " + mailId);
220        DeepaMehtaTransaction tx = dms.beginTx();
221        try {
222            // 1 clone mail ..
223            Topic mail = dms.getTopic(mailId).loadChildTopics();
224            ChildTopicsModel oldMail = mail.getModel().getChildTopicsModel();
225            String subject = oldMail.getString(SUBJECT);
226            String mailBody = oldMail.getString(BODY);
227            boolean fromDummy = oldMail.getBoolean(FROM);
228            String sentDate = "", messageId = ""; // nullify date and ID
229            // 1.1 re-use existing signatures (there is just one but see migration1 ###)
230            List<TopicModel> signatures = oldMail.getTopics(SIGNATURE);
231            long signatureId = -1;
232            for (TopicModel signature : signatures) {
233                signatureId = signature.getId();
234            }
235            ChildTopicsModel clonedMail = new ChildTopicsModel()
236                .put(SUBJECT, subject).put(BODY, mailBody)
237                .put(FROM, fromDummy).put(DATE, sentDate).put(MESSAGE_ID, messageId)
238                .addRef(SIGNATURE, signatureId); // do not copy signatures..
239            TopicModel model = new TopicModel(MAIL, clonedMail);
240            Topic clone = dms.createTopic(model);
241            
242            // 2 clone sender association ..
243            RelatedTopic sender = getSender(mail, true);
244            ChildTopics senderAssociation = sender.getRelatingAssociation().getChildTopics();
245            long addressId = senderAssociation.getTopic(EMAIL_ADDRESS).getId();
246            // 2.1 reference email address topic on new sender association
247            ChildTopicsModel newModel = new ChildTopicsModel()
248                .putRef(EMAIL_ADDRESS, addressId);
249            associateSender(clone.getId(), sender, newModel);
250            
251            // 3 clone recipient associations  ..
252            ResultList<RelatedTopic> recipientList = mail.getRelatedTopics(RECIPIENT, PARENT, CHILD, null, 0);
253            // migration note: fetchRelatingComposite = true
254            if (includeRecipients) {
255                for (RelatedTopic recipient : recipientList) {
256                    if (recipient.getTypeUri().equals(RECIPIENT) == false) {
257                        continue; // sender or something else found
258                    }
259                    // 3.1 re-use existing recipient types
260                    Association recipientAssociationModel = dms.getAssociation(recipient.getId()).loadChildTopics();
261                    ChildTopicsModel newValue = new ChildTopicsModel()
262                        .putRef(RECIPIENT_TYPE, recipientAssociationModel.getTopic(RECIPIENT_TYPE).getUri())
263                        .putRef(EMAIL_ADDRESS, recipientAssociationModel.getTopic(EMAIL_ADDRESS).getId());
264                    associateRecipient(clone.getId(), recipient, newValue);
265                }
266            }
267            tx.success();
268            return clone;
269        } finally {
270            tx.finish();
271        }
272    }
273
274    /**
275     * Creates a new mail to all email addresses of the contact topic.
276     *
277     * @param recipientId
278     *            ID of a recipient contact topic.
279     * @return Mail topic with associated recipient.
280     */
281    @POST
282    @Path("/write/{recipient}")
283    public Topic writeTo(@PathParam("recipient") long recipientId) {
284        log.info("write a mail to recipient " + recipientId);
285        DeepaMehtaTransaction tx = dms.beginTx();
286        try {
287            Topic mail = dms.createTopic(new TopicModel(MAIL));
288            Topic recipient = dms.getTopic(recipientId).loadChildTopics();
289            if (recipient.getChildTopics().has(EMAIL_ADDRESS)) {
290                for (Topic address : recipient.getChildTopics().getTopics(EMAIL_ADDRESS)) {
291                    associateRecipient(mail.getId(), address.getId(),//
292                            config.getDefaultRecipientType());
293                }
294            }
295            tx.success();
296            return mail;
297        } finally {
298            tx.finish();
299        }
300    }
301
302    /**
303     * @return Default recipient type.
304     */
305    @GET
306    @Path("/recipient/default")
307    public String getDefaultRecipientType() {
308        return config.getDefaultRecipientType().getUri();
309    }
310
311    /**
312     * @return Recipient types.
313     */
314    @GET
315    @Path("/recipient/types")
316    public ResultList<RelatedTopic> getRecipientTypes() {
317        return config.getRecipientTypes();
318    }
319
320    /**
321     * @see #getSearchParentTypes
322     */
323    @GET
324    @Path("/search/parents")
325    public ResultList<Topic> listSearchParentTypes() {
326        Collection<Topic> parents = getSearchParentTypes();
327        return new ResultList<Topic>(parents.size(), new ArrayList<Topic>(parents));
328    }
329
330    @Override
331    public Collection<Topic> getSearchParentTypes() {
332        return config.getSearchParentTypes();
333    }
334
335    /**
336     * Load the configuration.
337     *
338     * Useful after type and configuration changes with the web-client.
339     */
340    @GET
341    @Path("/config/load")
342    public Topic loadConfiguration() {
343        log.info("load mail configuration");
344        config = new MailConfigurationCache(dms);
345        autocomplete = new Autocomplete(dms, config);
346        return config.getTopic();
347    }
348
349    /**
350     * Sets the default sender and signature of a mail topic after creation.
351     */
352    @Override
353    public void postCreateTopic(Topic topic) {
354        if (topic.getTypeUri().equals(MAIL)) {
355            if (topic.getChildTopics().has(FROM) == false) { // new mail
356                associateDefaultSender(topic);
357            } else { // copied mail
358                Topic from = topic.getChildTopics().getTopic(FROM);
359                if (from.getSimpleValue().booleanValue() == false) { // sender?
360                    associateDefaultSender(topic);
361                }
362            }
363        }
364    }
365
366    /**
367     * @see #send(Mail)
368     */
369    @POST
370    @Path("/{mail}/send")
371    public StatusReport send(//
372            @PathParam("mail") long mailId) {
373        log.info("send mail " + mailId);
374        return send(new Mail(mailId, dms));
375    }
376
377    @Override
378    public StatusReport send(Mail mail) {
379        
380        log.info("DEBUG: Current classloader of MailPlugin is: " 
381            + Thread.currentThread().getContextClassLoader().toString());
382        // Thread.currentThread().setContextClassLoader(MailPlugin.class.getClassLoader());
383        // log.info("DEBUG: Quickfixed classloader of MailPlugin is: " 
384            // + Thread.currentThread().getContextClassLoader().toString());
385        StatusReport statusReport = new StatusReport(mail.getTopic());
386
387        HtmlEmail email = new HtmlEmail();
388        email.setDebug(true); // => System.out.println(SMTP communication);
389        email.setHostName(config.getSmtpHost());
390
391        try {
392            InternetAddress sender = mail.getSender();
393            // ..) Set Senders of Mail
394            email.setFrom(sender.getAddress(), sender.getPersonal());
395        } catch (UnsupportedEncodingException e) {
396            reportException(statusReport, Level.INFO, MailError.SENDER, e);
397        } catch (AddressException e) {
398            reportException(statusReport, Level.INFO, MailError.SENDER, e);
399        } catch (EmailException e) {
400            reportException(statusReport, Level.INFO, MailError.SENDER, e);
401        }
402
403        try {
404            String subject = mail.getSubject();
405            if (subject.isEmpty()) { // caught immediately
406                throw new IllegalArgumentException("Subject of mail is empty");
407            }
408            // ..) Set Subject of Mail
409            email.setSubject(subject);
410        } catch (Exception e) {
411            reportException(statusReport, Level.INFO, MailError.CONTENT, e);
412        }
413
414        try {
415            Document body = cidEmbedment.embedImages(email, mail.getBody());
416            String text = body.text();
417            if (text.isEmpty()) { // caught immediately
418                throw new IllegalArgumentException("Text body of mail is empty");
419            }
420            // ..) Set Message Body
421            email.setTextMsg(text);
422            email.setHtmlMsg(body.html());
423        } catch (Exception e) {
424            reportException(statusReport, Level.INFO, MailError.CONTENT, e);
425        }
426
427        for (Long fileId : mail.getAttachmentIds()) {
428            try {
429                String path = fileService.getFile(fileId).getAbsolutePath();
430                EmailAttachment attachment = new EmailAttachment();
431                attachment.setPath(path);
432                log.fine("attach " + path);
433                // ..) Attach Attachmentss
434                email.attach(attachment);
435            } catch (Exception e) {
436                reportException(statusReport, Level.INFO, MailError.ATTACHMENTS, e);
437            }
438        }
439
440        RecipientsByType recipients = new RecipientsByType();
441        try {
442            recipients = mail.getRecipients();
443            try {
444                // ..) Mapping Recipients
445                mapRecipients(email, recipients);
446            } catch (Exception e) {
447                reportException(statusReport, Level.SEVERE, MailError.RECIPIENT_TYPE, e);
448            }
449        } catch (InvalidRecipients e) {
450            for (String recipient : e.getRecipients()) {
451                log.log(Level.INFO, MailError.RECIPIENTS.getMessage() + ": " + recipient);
452                statusReport.addError(MailError.RECIPIENTS, recipient);
453            }
454        }
455
456        if (statusReport.hasErrors()) {
457            statusReport.setMessage("Mail can NOT be sent");
458        } else { // send, update message ID and return status with attached mail
459            try {
460                String messageId = email.send();
461                statusReport.setMessage("Mail was SUCCESSFULLY sent to " + //
462                        recipients.getCount() + " mail addresses");
463                mail.setMessageId(messageId);
464            } catch (EmailException e) {
465                statusReport.setMessage("Sending mail FAILED");
466                reportException(statusReport, Level.SEVERE, MailError.SEND, e);
467            } catch (Exception e) { // error after send
468                reportException(statusReport, Level.SEVERE, MailError.UPDATE, e);
469            }
470        }
471        return statusReport;
472    }
473
474    /**
475     * Initialize.
476     */
477    @Override
478    public void init() {
479        isInitialized = true;
480        configureIfReady();
481    }
482
483    private void configureIfReady() {
484        if (isInitialized && acService != null && fileService != null) {
485            createAttachmentDirectory();
486            checkACLsOfMigration();
487            loadConfiguration();
488        }
489    }
490
491    private void checkACLsOfMigration() {
492        Topic config = dms.getTopic("uri", new SimpleValue("dm4.mail.config"));
493        if (acService.getCreator(config) == null) {
494            DeepaMehtaTransaction tx = dms.beginTx();
495            log.info("initial ACL update of configuration");
496            try {
497                Topic admin = acService.getUsername("admin");
498                String adminName = admin.getSimpleValue().toString();
499                acService.setCreator(config, adminName);
500                acService.setOwner(config, adminName);
501                acService.setACL(config, new AccessControlList(new ACLEntry(Operation.WRITE, UserRole.OWNER)));
502                tx.success();
503            } catch (Exception e) {
504                log.warning("could not update ACLs of migration due to a " 
505                    +  e.getClass().toString());
506            } finally {
507                tx.finish();
508            }
509            
510        }
511    }
512
513    private void createAttachmentDirectory() {
514        // TODO move the initialization to migration "0"
515        try {
516            ResourceInfo resourceInfo = fileService.getResourceInfo(ATTACHMENTS);
517            String kind = resourceInfo.toJSON().getString("kind");
518            if (kind.equals("directory") == false) {
519                String repoPath = System.getProperty("dm4.filerepo.path");
520                String message = "attachment storage directory " + repoPath + File.separator + ATTACHMENTS
521                        + " can not be used";
522                throw new IllegalStateException(message);
523            }
524        } catch (WebApplicationException e) { // !exists
525            // catch fileService info request error => create directory
526            if (e.getResponse().getStatus() != 404) {
527                throw e;
528            } else {
529                log.info("create attachment directory");
530                fileService.createFolder(ATTACHMENTS, "/");
531            }
532        } catch (JSONException e) {
533            throw new RuntimeException(e);
534        }
535    }
536
537    /**
538     * @param mail
539     * @param clientState
540     * 
541     * @see MailPlugin#postCreateTopic(Topic)
542     */
543    private void associateDefaultSender(Topic mail) {
544        Topic creator = null;
545        RelatedTopic sender = null;
546
547        Topic creatorName = acService.getUsername(acService.getCreator(mail));
548        if (creatorName != null) {
549            creator = creatorName.getRelatedTopic(null, CHILD, PARENT, USER_ACCOUNT);
550        }
551
552        // get user account specific sender
553        if (creator != null) {
554            sender = getSender(creator, true);
555        }
556
557        // get the configured default sender instead
558        if (sender == null) {
559            sender = config.getDefaultSender();
560        }
561
562        if (sender != null) {
563            DeepaMehtaTransaction tx = dms.beginTx();
564            try {
565                ChildTopics value = sender.getRelatingAssociation().getChildTopics();
566                long addressId = value.getTopic(EMAIL_ADDRESS).getId();
567                ChildTopicsModel newValue = new ChildTopicsModel()
568                    .putRef(EMAIL_ADDRESS, addressId); // re-use existing e-mail address topics
569                associateSender(mail.getId(), sender, newValue);
570                RelatedTopic signature = getContactSignature(sender, addressId);
571                if (signature != null) {
572                    mail.getChildTopics().getModel().add(SIGNATURE, signature.getModel());
573                }
574                tx.success();
575            } finally {
576                tx.finish();
577            }
578        }
579    }
580
581    private Association associateRecipient(long topicId, Topic recipient, ChildTopicsModel value) {
582        DeepaMehtaTransaction tx = dms.beginTx();
583        try {
584            Association assoc = dms.createAssociation(new AssociationModel(RECIPIENT,//
585                new TopicRoleModel(recipient.getId(), CHILD),//
586                new TopicRoleModel(topicId, PARENT), value));
587            tx.success();
588            return assoc;
589        } finally {
590            tx.finish();
591        }
592    }
593
594    private Association associateSender(long topicId, Topic sender, ChildTopicsModel value) {
595        DeepaMehtaTransaction tx = dms.beginTx();
596        try {
597            Association assoc = dms.createAssociation(new AssociationModel(SENDER,//
598                new TopicRoleModel(sender.getId(), CHILD),//
599                new TopicRoleModel(topicId, PARENT), value));
600            tx.success();
601            return assoc;
602        } finally {
603            tx.finish();
604        }
605    }
606
607    private RelatedTopic getContactOfEmail(long addressId) {
608        return dms.getTopic(addressId).getRelatedTopic(COMPOSITION, CHILD, PARENT, null);
609    }
610
611    /** Comparing contact-signatures by "E-Mail Address"-Topic ID*/
612    private RelatedTopic getContactSignature(Topic topic, long addressId) {
613        for (RelatedTopic signature : topic.getRelatedTopics(SENDER, CHILD, PARENT, SIGNATURE, 0)) {
614            // signature had fetchComposite = true
615            signature.loadChildTopics();
616            // and fetchRelatingComposite = true
617            ChildTopics value = signature.getRelatingAssociation().loadChildTopics().getChildTopics();
618            if (addressId == value.getTopic(EMAIL_ADDRESS).getId()) {
619                return signature;
620            }
621        }
622        return null;
623    }
624
625    private Association getRecipientAssociation(long topicId, long addressId, long recipientId) {
626        for (Association recipient : dms.getAssociations(topicId, recipientId)) {
627            if (recipient.getTypeUri().equals(RECIPIENT)) {
628                Association association = dms.getAssociation(recipient.getId());
629                association.loadChildTopics();
630                Topic address = association.getChildTopics().getTopic(EMAIL_ADDRESS);
631                if (address.getId() == addressId) {
632                    return association;
633                }                
634            }
635        }
636        return null;
637    }
638
639    private RelatedTopic getSender(long topicId, boolean fetchRelatingComposite) {
640        return getSender(dms.getTopic(topicId), fetchRelatingComposite);
641    }
642
643    private RelatedTopic getSender(Topic topic, boolean fetchRelatingComposite) {
644        if (!fetchRelatingComposite) {
645            return topic.getRelatedTopic(SENDER, PARENT, CHILD, null);   
646        } else {
647            // do fetch relating composite
648            RelatedTopic sender = topic.getRelatedTopic(SENDER, PARENT, CHILD, null);
649            if (sender != null) { // this may be the case when a new mail is instantiated 
650                sender.getAssociation(SENDER, CHILD, PARENT, topic.getId()).loadChildTopics();
651            }
652            return sender;
653        }
654    }
655
656    private Association getSenderAssociation(long topicId, long senderId) {
657        return dms.getAssociation(SENDER, topicId, senderId, PARENT, CHILD).loadChildTopics();
658    }
659
660    private void mapRecipients(HtmlEmail email, Map<RecipientType, List<InternetAddress>> recipients)
661            throws EmailException {
662        for (RecipientType type : recipients.keySet()) {
663            switch (type) {
664            case BCC:
665                email.setBcc(recipients.get(type));
666                break;
667            case CC:
668                email.setCc(recipients.get(type));
669                break;
670            case TO:
671                email.setTo(recipients.get(type));
672                break;
673            default:
674                throw new IllegalArgumentException(type.toString());
675            }
676        }
677    }
678
679    private void reportException(StatusReport report, Level level, MailError error, Exception e) {
680        String message = e.getMessage();
681        Throwable cause = e.getCause();
682        if (cause != null) {
683            message += ": " + cause.getMessage();
684        }
685        String logMessage = error.getMessage() + ": " + message;
686        if (level == Level.WARNING || level == Level.SEVERE) {
687            log.log(level, logMessage, e); // log the exception trace
688        } else {
689            log.log(level, logMessage); // log only the message
690        }
691        report.addError(error, message);
692    }
693    
694    @Override
695    public void serviceArrived(PluginService service) {
696        if (service instanceof FilesService) {
697            cidEmbedment = new ImageCidEmbedment(fileService);
698        }
699    }
700    
701    @Override
702    public void serviceGone(PluginService service) {
703        if (service instanceof FilesService) {
704            cidEmbedment = null;
705        }
706    }
707
708}