001    /**
002     * Copyright (c) 2000-2013 Liferay, Inc. All rights reserved.
003     *
004     * This library is free software; you can redistribute it and/or modify it under
005     * the terms of the GNU Lesser General Public License as published by the Free
006     * Software Foundation; either version 2.1 of the License, or (at your option)
007     * any later version.
008     *
009     * This library is distributed in the hope that it will be useful, but WITHOUT
010     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
011     * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
012     * details.
013     */
014    
015    package com.liferay.portal.util;
016    
017    import com.liferay.mail.model.FileAttachment;
018    import com.liferay.mail.service.MailServiceUtil;
019    import com.liferay.portal.kernel.exception.PortalException;
020    import com.liferay.portal.kernel.log.Log;
021    import com.liferay.portal.kernel.log.LogFactoryUtil;
022    import com.liferay.portal.kernel.mail.MailMessage;
023    import com.liferay.portal.kernel.mail.SMTPAccount;
024    import com.liferay.portal.kernel.messaging.DestinationNames;
025    import com.liferay.portal.kernel.messaging.MessageBusUtil;
026    import com.liferay.portal.kernel.transaction.TransactionCommitCallbackRegistryUtil;
027    import com.liferay.portal.kernel.util.ClassLoaderPool;
028    import com.liferay.portal.kernel.util.EscapableObject;
029    import com.liferay.portal.kernel.util.GetterUtil;
030    import com.liferay.portal.kernel.util.HtmlEscapableObject;
031    import com.liferay.portal.kernel.util.HtmlUtil;
032    import com.liferay.portal.kernel.util.LocaleUtil;
033    import com.liferay.portal.kernel.util.ObjectValuePair;
034    import com.liferay.portal.kernel.util.StringPool;
035    import com.liferay.portal.kernel.util.StringUtil;
036    import com.liferay.portal.kernel.util.Validator;
037    import com.liferay.portal.model.Company;
038    import com.liferay.portal.model.Group;
039    import com.liferay.portal.model.Subscription;
040    import com.liferay.portal.model.User;
041    import com.liferay.portal.security.permission.PermissionChecker;
042    import com.liferay.portal.security.permission.PermissionCheckerFactoryUtil;
043    import com.liferay.portal.service.CompanyLocalServiceUtil;
044    import com.liferay.portal.service.GroupLocalServiceUtil;
045    import com.liferay.portal.service.ServiceContext;
046    import com.liferay.portal.service.SubscriptionLocalServiceUtil;
047    import com.liferay.portal.service.UserLocalServiceUtil;
048    import com.liferay.portal.service.permission.SubscriptionPermissionUtil;
049    
050    import java.io.File;
051    import java.io.IOException;
052    import java.io.ObjectInputStream;
053    import java.io.ObjectOutputStream;
054    import java.io.Serializable;
055    
056    import java.util.ArrayList;
057    import java.util.HashMap;
058    import java.util.HashSet;
059    import java.util.List;
060    import java.util.Locale;
061    import java.util.Map;
062    import java.util.Set;
063    import java.util.concurrent.Callable;
064    
065    import javax.mail.internet.InternetAddress;
066    
067    /**
068     * @author Brian Wing Shun Chan
069     * @author Mate Thurzo
070     * @author Raymond Aug??
071     */
072    public class SubscriptionSender implements Serializable {
073    
074            public void addFileAttachment(File file) {
075                    addFileAttachment(file, null);
076            }
077    
078            public void addFileAttachment(File file, String fileName) {
079                    if (file == null) {
080                            return;
081                    }
082    
083                    if (fileAttachments == null) {
084                            fileAttachments = new ArrayList<FileAttachment>();
085                    }
086    
087                    FileAttachment attachment = new FileAttachment(file, fileName);
088    
089                    fileAttachments.add(attachment);
090            }
091    
092            public void addPersistedSubscribers(String className, long classPK) {
093                    ObjectValuePair<String, Long> ovp = new ObjectValuePair<String, Long>(
094                            className, classPK);
095    
096                    _persistestedSubscribersOVPs.add(ovp);
097            }
098    
099            public void addRuntimeSubscribers(String toAddress, String toName) {
100                    ObjectValuePair<String, String> ovp =
101                            new ObjectValuePair<String, String>(toAddress, toName);
102    
103                    _runtimeSubscribersOVPs.add(ovp);
104            }
105    
106            public void flushNotifications() throws Exception {
107                    initialize();
108    
109                    Thread currentThread = Thread.currentThread();
110    
111                    ClassLoader contextClassLoader = currentThread.getContextClassLoader();
112    
113                    try {
114                            if ((_classLoader != null) &&
115                                    (contextClassLoader != _classLoader)) {
116    
117                                    currentThread.setContextClassLoader(_classLoader);
118                            }
119    
120                            String inferredClassName = null;
121                            long inferredClassPK = 0;
122    
123                            if (_persistestedSubscribersOVPs.size() > 1) {
124                                    ObjectValuePair<String, Long> objectValuePair =
125                                            _persistestedSubscribersOVPs.get(
126                                                    _persistestedSubscribersOVPs.size() - 1);
127    
128                                    inferredClassName = objectValuePair.getKey();
129                                    inferredClassPK = objectValuePair.getValue();
130                            }
131    
132                            for (ObjectValuePair<String, Long> ovp :
133                                            _persistestedSubscribersOVPs) {
134    
135                                    String className = ovp.getKey();
136                                    long classPK = ovp.getValue();
137    
138                                    List<Subscription> subscriptions =
139                                            SubscriptionLocalServiceUtil.getSubscriptions(
140                                                    companyId, className, classPK);
141    
142                                    for (Subscription subscription : subscriptions) {
143                                            try {
144                                                    notifySubscriber(
145                                                            subscription, inferredClassName, inferredClassPK);
146                                            }
147                                            catch (PortalException pe) {
148                                                    _log.error(
149                                                            "Unable to process subscription: " + subscription);
150                                            }
151                                    }
152    
153                                    if (bulk) {
154                                            Locale locale = LocaleUtil.getDefault();
155    
156                                            InternetAddress to = new InternetAddress(
157                                                    replaceContent(replyToAddress, locale),
158                                                    replaceContent(replyToAddress, locale));
159    
160                                            sendEmail(to, locale);
161                                    }
162                            }
163    
164                            _persistestedSubscribersOVPs.clear();
165    
166                            for (ObjectValuePair<String, String> ovp :
167                                            _runtimeSubscribersOVPs) {
168    
169                                    String toAddress = ovp.getKey();
170    
171                                    if (Validator.isNull(toAddress)) {
172                                            continue;
173                                    }
174    
175                                    if (_sentEmailAddresses.contains(toAddress)) {
176                                            if (_log.isDebugEnabled()) {
177                                                    _log.debug(
178                                                            "Do not send a duplicate email to " + toAddress);
179                                            }
180    
181                                            continue;
182                                    }
183    
184                                    if (_log.isDebugEnabled()) {
185                                            _log.debug(
186                                                    "Add " + toAddress + " to the list of users who " +
187                                                            "have received an email");
188                                    }
189    
190                                    _sentEmailAddresses.add(toAddress);
191    
192                                    String toName = ovp.getValue();
193    
194                                    InternetAddress to = new InternetAddress(toAddress, toName);
195    
196                                    sendEmail(to, LocaleUtil.getDefault());
197                            }
198    
199                            _runtimeSubscribersOVPs.clear();
200                    }
201                    finally {
202                            if ((_classLoader != null) &&
203                                    (contextClassLoader != _classLoader)) {
204    
205                                    currentThread.setContextClassLoader(contextClassLoader);
206                            }
207                    }
208            }
209    
210            public void flushNotificationsAsync() {
211                    TransactionCommitCallbackRegistryUtil.registerCallback(
212                            new Callable<Void>() {
213    
214                                    @Override
215                                    public Void call() throws Exception {
216                                            Thread currentThread = Thread.currentThread();
217    
218                                            _classLoader = currentThread.getContextClassLoader();
219    
220                                            MessageBusUtil.sendMessage(
221                                                    DestinationNames.SUBSCRIPTION_SENDER,
222                                                    SubscriptionSender.this);
223    
224                                            return null;
225                                    }
226                            }
227                    );
228            }
229    
230            public Object getContextAttribute(String key) {
231                    return _context.get(key);
232            }
233    
234            public String getMailId() {
235                    return this.mailId;
236            }
237    
238            public void initialize() throws Exception {
239                    if (_initialized) {
240                            return;
241                    }
242    
243                    _initialized = true;
244    
245                    if ((groupId == 0) && (serviceContext != null)) {
246                            setScopeGroupId(serviceContext.getScopeGroupId());
247                    }
248    
249                    Company company = CompanyLocalServiceUtil.getCompany(companyId);
250    
251                    setContextAttribute("[$COMPANY_ID$]", company.getCompanyId());
252                    setContextAttribute("[$COMPANY_MX$]", company.getMx());
253                    setContextAttribute("[$COMPANY_NAME$]", company.getName());
254                    setContextAttribute("[$PORTAL_URL$]", company.getPortalURL(groupId));
255    
256                    if (groupId > 0) {
257                            Group group = GroupLocalServiceUtil.getGroup(groupId);
258    
259                            setContextAttribute("[$SITE_NAME$]", group.getDescriptiveName());
260                    }
261    
262                    if ((userId > 0) && Validator.isNotNull(_contextUserPrefix)) {
263                            setContextAttribute(
264                                    "[$" + _contextUserPrefix + "_USER_ADDRESS$]",
265                                    PortalUtil.getUserEmailAddress(userId));
266                            setContextAttribute(
267                                    "[$" + _contextUserPrefix + "_USER_NAME$]",
268                                    PortalUtil.getUserName(userId, StringPool.BLANK));
269                    }
270    
271                    mailId = PortalUtil.getMailId(
272                            company.getMx(), _mailIdPopPortletPrefix, _mailIdIds);
273            }
274    
275            public void setBody(String body) {
276                    this.body = body;
277            }
278    
279            public void setBulk(boolean bulk) {
280                    this.bulk = bulk;
281            }
282    
283            public void setCompanyId(long companyId) {
284                    this.companyId = companyId;
285            }
286    
287            public void setContextAttribute(String key, EscapableObject<String> value) {
288                    _context.put(key, value);
289            }
290    
291            public void setContextAttribute(String key, Object value) {
292                    setContextAttribute(key, value, true);
293            }
294    
295            public void setContextAttribute(String key, Object value, boolean escape) {
296                    setContextAttribute(
297                            key,
298                            new HtmlEscapableObject<String>(String.valueOf(value), escape));
299            }
300    
301            public void setContextAttributes(Object... values) {
302                    for (int i = 0; i < values.length; i += 2) {
303                            setContextAttribute(String.valueOf(values[i]), values[i + 1]);
304                    }
305            }
306    
307            public void setContextUserPrefix(String contextUserPrefix) {
308                    _contextUserPrefix = contextUserPrefix;
309            }
310    
311            public void setFrom(String fromAddress, String fromName) {
312                    this.fromAddress = fromAddress;
313                    this.fromName = fromName;
314            }
315    
316            public void setGroupId(long groupId) {
317                    this.groupId = groupId;
318            }
319    
320            public void setHtmlFormat(boolean htmlFormat) {
321                    this.htmlFormat = htmlFormat;
322            }
323    
324            public void setInReplyTo(String inReplyTo) {
325                    this.inReplyTo = inReplyTo;
326            }
327    
328            public void setLocalizedBodyMap(Map<Locale, String> localizedBodyMap) {
329                    this.localizedBodyMap = localizedBodyMap;
330            }
331    
332            public void setLocalizedSubjectMap(
333                    Map<Locale, String> localizedSubjectMap) {
334    
335                    this.localizedSubjectMap = localizedSubjectMap;
336            }
337    
338            public void setMailId(String popPortletPrefix, Object... ids) {
339                    _mailIdPopPortletPrefix = popPortletPrefix;
340                    _mailIdIds = ids;
341            }
342    
343            public void setPortletId(String portletId) {
344                    this.portletId = portletId;
345            }
346    
347            public void setReplyToAddress(String replyToAddress) {
348                    this.replyToAddress = replyToAddress;
349            }
350    
351            /**
352             * @see com.liferay.portal.kernel.search.BaseIndexer#getSiteGroupId(long)
353             */
354            public void setScopeGroupId(long scopeGroupId) {
355                    try {
356                            Group group = GroupLocalServiceUtil.getGroup(scopeGroupId);
357    
358                            if (group.isLayout()) {
359                                    groupId = group.getParentGroupId();
360                            }
361                            else {
362                                    groupId = scopeGroupId;
363                            }
364                    }
365                    catch (Exception e) {
366                    }
367    
368                    this.scopeGroupId = scopeGroupId;
369            }
370    
371            public void setServiceContext(ServiceContext serviceContext) {
372                    this.serviceContext = serviceContext;
373            }
374    
375            public void setSMTPAccount(SMTPAccount smtpAccount) {
376                    this.smtpAccount = smtpAccount;
377            }
378    
379            public void setSubject(String subject) {
380                    this.subject = subject;
381            }
382    
383            public void setUserId(long userId) {
384                    this.userId = userId;
385            }
386    
387            protected void deleteSubscription(Subscription subscription)
388                    throws Exception {
389    
390                    SubscriptionLocalServiceUtil.deleteSubscription(
391                            subscription.getSubscriptionId());
392            }
393    
394            protected boolean hasPermission(
395                            Subscription subscription, String inferredClassName,
396                            long inferredClassPK, User user)
397                    throws Exception {
398    
399                    PermissionChecker permissionChecker =
400                            PermissionCheckerFactoryUtil.create(user);
401    
402                    return SubscriptionPermissionUtil.contains(
403                            permissionChecker, subscription.getClassName(),
404                            subscription.getClassPK(), inferredClassName, inferredClassPK);
405            }
406    
407            /**
408             * @deprecated As of 6.2.0, replaced by {@link #hasPermission(Subscription,
409             *             String, long, User)}
410             */
411            protected boolean hasPermission(Subscription subscription, User user)
412                    throws Exception {
413    
414                    return hasPermission(subscription, null, 0, user);
415            }
416    
417            /**
418             * @deprecated As of 6.2.0, replaced by {@link
419             *             #notifySubscriber(Subscription, String, long)}
420             */
421            protected void notifySubscriber(Subscription subscription)
422                    throws Exception {
423    
424                    notifySubscriber(subscription, null, 0);
425            }
426    
427            protected void notifySubscriber(
428                            Subscription subscription, String inferredClassName,
429                            long inferredClassPK)
430                    throws Exception {
431    
432                    User user = UserLocalServiceUtil.fetchUserById(
433                            subscription.getUserId());
434    
435                    if (user == null) {
436                            if (_log.isInfoEnabled()) {
437                                    _log.info(
438                                            "Subscription " + subscription.getSubscriptionId() +
439                                                    " is stale and will be deleted");
440                            }
441    
442                            deleteSubscription(subscription);
443    
444                            return;
445                    }
446    
447                    String emailAddress = user.getEmailAddress();
448    
449                    if (_sentEmailAddresses.contains(emailAddress)) {
450                            if (_log.isDebugEnabled()) {
451                                    _log.debug("Do not send a duplicate email to " + emailAddress);
452                            }
453    
454                            return;
455                    }
456                    else {
457                            if (_log.isDebugEnabled()) {
458                                    _log.debug(
459                                            "Add " + emailAddress +
460                                                    " to the list of users who have received an email");
461                            }
462    
463                            _sentEmailAddresses.add(emailAddress);
464                    }
465    
466                    if (!user.isActive()) {
467                            if (_log.isDebugEnabled()) {
468                                    _log.debug("Skip inactive user " + user.getUserId());
469                            }
470    
471                            return;
472                    }
473    
474                    try {
475                            if (!hasPermission(
476                                            subscription, inferredClassName, inferredClassPK, user)) {
477    
478                                    if (_log.isDebugEnabled()) {
479                                            _log.debug("Skip unauthorized user " + user.getUserId());
480                                    }
481    
482                                    return;
483                            }
484                    }
485                    catch (Exception e) {
486                            _log.error(e, e);
487    
488                            return;
489                    }
490    
491                    if (bulk) {
492                            InternetAddress bulkAddress = new InternetAddress(
493                                    user.getEmailAddress(), user.getFullName());
494    
495                            if (_bulkAddresses == null) {
496                                    _bulkAddresses = new ArrayList<InternetAddress>();
497                            }
498    
499                            _bulkAddresses.add(bulkAddress);
500                    }
501                    else {
502                            try {
503                                    InternetAddress to = new InternetAddress(
504                                            user.getEmailAddress(), user.getFullName());
505    
506                                    sendEmail(to, user.getLocale());
507                            }
508                            catch (Exception e) {
509                                    _log.error(e, e);
510                            }
511                    }
512            }
513    
514            protected void processMailMessage(MailMessage mailMessage, Locale locale)
515                    throws Exception {
516    
517                    InternetAddress from = mailMessage.getFrom();
518                    InternetAddress to = mailMessage.getTo()[0];
519    
520                    String processedSubject = StringUtil.replace(
521                            mailMessage.getSubject(),
522                            new String[] {
523                                    "[$FROM_ADDRESS$]", "[$FROM_NAME$]", "[$TO_ADDRESS$]",
524                                    "[$TO_NAME$]"
525                            },
526                            new String[] {
527                                    from.getAddress(),
528                                    GetterUtil.getString(from.getPersonal(), from.getAddress()),
529                                    HtmlUtil.escape(to.getAddress()),
530                                    HtmlUtil.escape(
531                                            GetterUtil.getString(to.getPersonal(), to.getAddress()))
532                            });
533    
534                    processedSubject = replaceContent(processedSubject, locale, false);
535    
536                    mailMessage.setSubject(processedSubject);
537    
538                    String processedBody = StringUtil.replace(
539                            mailMessage.getBody(),
540                            new String[] {
541                                    "[$FROM_ADDRESS$]", "[$FROM_NAME$]", "[$TO_ADDRESS$]",
542                                    "[$TO_NAME$]"
543                            },
544                            new String[] {
545                                    from.getAddress(),
546                                    GetterUtil.getString(from.getPersonal(), from.getAddress()),
547                                    HtmlUtil.escape(to.getAddress()),
548                                    HtmlUtil.escape(
549                                            GetterUtil.getString(to.getPersonal(), to.getAddress()))
550                            });
551    
552                    processedBody = replaceContent(processedBody, locale, htmlFormat);
553    
554                    mailMessage.setBody(processedBody);
555            }
556    
557            protected String replaceContent(String content, Locale locale)
558                    throws Exception {
559    
560                    return replaceContent(content, locale, true);
561            }
562    
563            protected String replaceContent(
564                            String content, Locale locale, boolean escape)
565                    throws Exception {
566    
567                    for (Map.Entry<String, EscapableObject<String>> entry :
568                                    _context.entrySet()) {
569    
570                            String key = entry.getKey();
571                            EscapableObject<String> value = entry.getValue();
572    
573                            String valueString = null;
574    
575                            if (escape) {
576                                    valueString = value.getEscapedValue();
577                            }
578                            else {
579                                    valueString = value.getOriginalValue();
580                            }
581    
582                            content = StringUtil.replace(content, key, valueString);
583                    }
584    
585                    if (Validator.isNotNull(portletId)) {
586                            String portletName = PortalUtil.getPortletTitle(portletId, locale);
587    
588                            content = StringUtil.replace(
589                                    content, "[$PORTLET_NAME$]", portletName);
590                    }
591    
592                    Company company = CompanyLocalServiceUtil.getCompany(companyId);
593    
594                    content = StringUtil.replace(
595                            content,
596                            new String[] {
597                                    "href=\"/", "src=\"/"
598                            },
599                            new String[] {
600                                    "href=\"" + company.getPortalURL(groupId) + "/",
601                                    "src=\"" + company.getPortalURL(groupId) + "/"
602                            });
603    
604                    return content;
605            }
606    
607            protected void sendEmail(InternetAddress to, Locale locale)
608                    throws Exception {
609    
610                    InternetAddress from = new InternetAddress(
611                            replaceContent(fromAddress, locale),
612                            replaceContent(fromName, locale));
613    
614                    String processedSubject = null;
615    
616                    if (localizedSubjectMap != null) {
617                            String localizedSubject = localizedSubjectMap.get(locale);
618    
619                            if (Validator.isNull(localizedSubject)) {
620                                    Locale defaultLocale = LocaleUtil.getDefault();
621    
622                                    processedSubject = localizedSubjectMap.get(defaultLocale);
623                            }
624                            else {
625                                    processedSubject = localizedSubject;
626                            }
627                    }
628                    else {
629                            processedSubject = this.subject;
630                    }
631    
632                    String processedBody = null;
633    
634                    if (localizedBodyMap != null) {
635                            String localizedBody = localizedBodyMap.get(locale);
636    
637                            if (Validator.isNull(localizedBody)) {
638                                    Locale defaultLocale = LocaleUtil.getDefault();
639    
640                                    processedBody = localizedBodyMap.get(defaultLocale);
641                            }
642                            else {
643                                    processedBody = localizedBody;
644                            }
645                    }
646                    else {
647                            processedBody = this.body;
648                    }
649    
650                    MailMessage mailMessage = new MailMessage(
651                            from, to, processedSubject, processedBody, htmlFormat);
652    
653                    if (fileAttachments != null) {
654                            for (FileAttachment fileAttachment : fileAttachments) {
655                                    mailMessage.addFileAttachment(
656                                            fileAttachment.getFile(), fileAttachment.getFileName());
657                            }
658                    }
659    
660                    if (bulk && (_bulkAddresses != null)) {
661                            mailMessage.setBulkAddresses(
662                                    _bulkAddresses.toArray(
663                                            new InternetAddress[_bulkAddresses.size()]));
664    
665                            _bulkAddresses.clear();
666                    }
667    
668                    if (inReplyTo != null) {
669                            mailMessage.setInReplyTo(inReplyTo);
670                    }
671    
672                    mailMessage.setMessageId(mailId);
673    
674                    if (replyToAddress != null) {
675                            InternetAddress replyTo = new InternetAddress(
676                                    replaceContent(replyToAddress, locale),
677                                    replaceContent(replyToAddress, locale));
678    
679                            mailMessage.setReplyTo(new InternetAddress[] {replyTo});
680                    }
681    
682                    if (smtpAccount != null) {
683                            mailMessage.setSMTPAccount(smtpAccount);
684                    }
685    
686                    processMailMessage(mailMessage, locale);
687    
688                    MailServiceUtil.sendEmail(mailMessage);
689            }
690    
691            protected String body;
692            protected boolean bulk;
693            protected long companyId;
694            protected List<FileAttachment> fileAttachments =
695                    new ArrayList<FileAttachment>();
696            protected String fromAddress;
697            protected String fromName;
698            protected long groupId;
699            protected boolean htmlFormat;
700            protected String inReplyTo;
701            protected Map<Locale, String> localizedBodyMap;
702            protected Map<Locale, String> localizedSubjectMap;
703            protected String mailId;
704            protected String portletId;
705            protected String replyToAddress;
706            protected long scopeGroupId;
707            protected ServiceContext serviceContext;
708            protected SMTPAccount smtpAccount;
709            protected String subject;
710            protected long userId;
711    
712            private void readObject(ObjectInputStream objectInputStream)
713                    throws ClassNotFoundException, IOException {
714    
715                    objectInputStream.defaultReadObject();
716    
717                    String servletContextName = objectInputStream.readUTF();
718    
719                    if (!servletContextName.isEmpty()) {
720                            _classLoader = ClassLoaderPool.getClassLoader(servletContextName);
721                    }
722            }
723    
724            private void writeObject(ObjectOutputStream objectOutputStream)
725                    throws IOException {
726    
727                    objectOutputStream.defaultWriteObject();
728    
729                    String servletContextName = StringPool.BLANK;
730    
731                    if (_classLoader != null) {
732                            servletContextName = ClassLoaderPool.getContextName(_classLoader);
733                    }
734    
735                    objectOutputStream.writeUTF(servletContextName);
736            }
737    
738            private static Log _log = LogFactoryUtil.getLog(SubscriptionSender.class);
739    
740            private List<InternetAddress> _bulkAddresses;
741            private transient ClassLoader _classLoader;
742            private Map<String, EscapableObject<String>> _context =
743                    new HashMap<String, EscapableObject<String>>();
744            private String _contextUserPrefix;
745            private boolean _initialized;
746            private Object[] _mailIdIds;
747            private String _mailIdPopPortletPrefix;
748            private List<ObjectValuePair<String, Long>> _persistestedSubscribersOVPs =
749                    new ArrayList<ObjectValuePair<String, Long>>();
750            private List<ObjectValuePair<String, String>> _runtimeSubscribersOVPs =
751                    new ArrayList<ObjectValuePair<String, String>>();
752            private Set<String> _sentEmailAddresses = new HashSet<String>();
753    
754    }