001: package org.tigris.scarab.util;
002:
003: /* ================================================================
004: * Copyright (c) 2000 CollabNet. All rights reserved.
005: *
006: * Redistribution and use in source and binary forms, with or without
007: * modification, are permitted provided that the following conditions are
008: * met:
009: *
010: * 1. Redistributions of source code must retain the above copyright
011: * notice, this list of conditions and the following disclaimer.
012: *
013: * 2. Redistributions in binary form must reproduce the above copyright
014: * notice, this list of conditions and the following disclaimer in the
015: * documentation and/or other materials provided with the distribution.
016: *
017: * 3. The end-user documentation included with the redistribution, if
018: * any, must include the following acknowlegement: "This product includes
019: * software developed by CollabNet (http://www.collab.net/)."
020: * Alternately, this acknowlegement may appear in the software itself, if
021: * and wherever such third-party acknowlegements normally appear.
022: *
023: * 4. The hosted project names must not be used to endorse or promote
024: * products derived from this software without prior written
025: * permission. For written permission, please contact info@collab.net.
026: *
027: * 5. Products derived from this software may not use the "Tigris" name
028: * nor may "Tigris" appear in their names without prior written
029: * permission of CollabNet.
030: *
031: * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
032: * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
033: * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
034: * IN NO EVENT SHALL COLLAB.NET OR ITS CONTRIBUTORS BE LIABLE FOR ANY
035: * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
036: * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
037: * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
038: * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
039: * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
040: * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
041: * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
042: *
043: * ====================================================================
044: *
045: * This software consists of voluntary contributions made by many
046: * individuals on behalf of CollabNet.
047: */
048:
049: import java.util.ArrayList;
050: import java.util.Collection;
051: import java.util.HashMap;
052: import java.util.HashSet;
053: import java.util.Iterator;
054: import java.util.List;
055: import java.util.Locale;
056: import java.util.Map;
057: import java.util.Set;
058: import java.util.StringTokenizer;
059:
060: import javax.mail.SendFailedException;
061: import javax.mail.internet.InternetAddress;
062:
063: import org.apache.fulcrum.ServiceException;
064: import org.apache.fulcrum.template.TemplateContext;
065: import org.apache.fulcrum.template.TemplateEmail;
066: import org.apache.fulcrum.velocity.ContextAdapter;
067: import org.apache.log4j.Logger;
068: import org.apache.torque.TorqueException;
069: import org.apache.turbine.Turbine;
070: import org.tigris.scarab.om.GlobalParameter;
071: import org.tigris.scarab.om.GlobalParameterManager;
072: import org.tigris.scarab.om.Module;
073: import org.tigris.scarab.om.ScarabUserManager;
074: import org.tigris.scarab.om.ScarabUser;
075: import org.tigris.scarab.services.email.VelocityEmail;
076: import org.tigris.scarab.tools.ScarabLocalizationTool;
077: import org.tigris.scarab.tools.localization.L10NKeySet;
078:
079: /**
080: * Sends a notification email.
081: *
082: * @author <a href="mailto:jon@collab.net">Jon Scott Stevens</a>
083: * @author <a href="mailto:elicia@collab.net">Elicia David</a>
084: * @author <a href="mailto:jmcnally@collab.net">John McNally</a>
085: * @version $Id: Email.java 10366 2006-11-29 18:38:43Z ronvoe122 $
086: */
087: public class Email extends TemplateEmail {
088: private static final int TO = 0;
089: private static final int CC = 1;
090: private static final Integer ARCHIVE_USER_ID = new Integer(-1234);
091: private static ScarabUser archiveUser;
092:
093: public static Logger log = Log.get(Email.class.getName());
094:
095: /**
096: * Sends email to a single recipient. Throws an Excetion,
097: * if it fails to send the email for any reason.
098: */
099: public static void sendEmail(EmailContext context, Module module,
100: Object fromUser, Object replyToUser, ScarabUser toUser,
101: String template) throws Exception {
102: Collection toUsers = new ArrayList(2);
103: toUsers.add(toUser);
104: sendEmail(context, module, fromUser, replyToUser, toUsers,
105: null, template);
106: }
107:
108: /**
109: * Sends email to multiple recipients. Throws an Exception,
110: * if it fails to send the email for any reason.
111: */
112: public static void sendEmail(EmailContext context, Module module,
113: Object fromUser, Object replyToUser, Collection toUsers,
114: Collection ccUsers, String template) throws Exception {
115: if (!GlobalParameterManager.getBoolean(
116: GlobalParameter.EMAIL_ENABLED, module)) {
117: return;
118: }
119:
120: //
121: // To avoid any NullPointerExceptions, create
122: // empty lists of to: and cc: users if the
123: // collections are null.
124: //
125: if (toUsers == null) {
126: toUsers = new ArrayList();
127: }
128:
129: if (ccUsers == null) {
130: ccUsers = new ArrayList();
131: }
132:
133: //
134: // Remove duplicate addresses from the cc: list
135: //
136: ccUsers.removeAll(toUsers);
137:
138: Map userLocaleMap = groupAddressesByLocale(module, toUsers,
139: ccUsers);
140:
141: for (Iterator i = userLocaleMap.keySet().iterator(); i
142: .hasNext();) {
143: Locale locale = (Locale) i.next();
144: List[] toAndCC = (List[]) userLocaleMap.get(locale);
145: List to = toAndCC[TO];
146: List cc = toAndCC[CC];
147:
148: sendEmailInLocale(context, module, fromUser, replyToUser,
149: to, cc, template, locale);
150: }
151:
152: }
153:
154: /** Sends email in a specific locale. */
155: private static void sendEmailInLocale(EmailContext context,
156: Module module, Object fromUser, Object replyToUser,
157: List toAddresses, List ccAddresses, String template,
158: Locale locale) throws Exception {
159: log.debug("Sending email for locale=" + locale);
160:
161: // get reference to l10n tool, so we can alter the locale per email
162: ScarabLocalizationTool l10n = new ScarabLocalizationTool();
163: context.setLocalizationTool(l10n);
164: l10n.init(locale);
165:
166: Email te = getEmail(context, module, fromUser, replyToUser,
167: template);
168: te.setCharset(getCharset(locale));
169:
170: boolean atLeastOneTo = false;
171: for (Iterator iTo = toAddresses.iterator(); iTo.hasNext();) {
172: InternetAddress a = (InternetAddress) iTo.next();
173: log.debug("Adding To: email[" + a.getAddress() + "], name["
174: + a.getPersonal() + "]");
175: te.addTo(a.getAddress(), a.getPersonal());
176: atLeastOneTo = true;
177: }
178: for (Iterator iCC = ccAddresses.iterator(); iCC.hasNext();) {
179: InternetAddress a = (InternetAddress) iCC.next();
180: String email = a.getAddress();
181: String name = a.getPersonal();
182:
183: // template email requires a To: user, it does seem possible
184: // to send emails with only a CC: user, so not sure if this
185: // is a bug to be fixed in TemplateEmail. Might not be good
186: // form anyway. So if there are no To: users, upgrade CC's.
187: if (atLeastOneTo) {
188: log.debug("Adding CC: email[" + email + "], name["
189: + name + "]");
190: te.addCc(email, name);
191: } else {
192: log.debug("Adding to: email[" + email + "], name["
193: + name + "]");
194: te.addTo(email, name);
195: // We've added one To: user and TemplateEmail should be
196: // happy. No need to move all CC: into TO:
197: atLeastOneTo = true;
198: }
199: }
200:
201: try {
202: log.debug("Sending email ...");
203: te.sendMultiple();
204: } catch (SendFailedException sfe) {
205: log.warn("Could not send Email. Cause [" + sfe.getMessage()
206: + "]");
207: if (sfe.getCause() != null) {
208: log.warn("Cause: [" + sfe.getCause().getMessage());
209: }
210: Throwable t = sfe.getNextException();
211: throw new ScarabException(L10NKeySet.ExceptionEmailFailure,
212: t);
213: }
214: }
215:
216: /**
217: * Creates a map of Locale objects -> List[2], where the first
218: * element of the list array is a list of "To:" addresses, and
219: * the second is a list of "Cc:" addresses. For example, if
220: * user "Pierre" is in <code>toUsers</code> and requires emails
221: * in french, his email address will be in
222: * <code>userLocaleMap[Locale.FRANCE][TO]</code>. The same applies
223: * to "Cc:" addresses, while the archive email address is associated
224: * with the default module locale.
225: */
226: private static Map groupAddressesByLocale(Module module,
227: Collection toUsers, Collection ccUsers) throws Exception {
228: Map result = new HashMap();
229: for (Iterator iter = toUsers.iterator(); iter.hasNext();) {
230: fileUser(result, (ScarabUser) iter.next(), module, TO);
231: }
232:
233: for (Iterator iter = ccUsers.iterator(); iter.hasNext();) {
234: fileUser(result, (ScarabUser) iter.next(), module, CC);
235: }
236: return result;
237: }
238:
239: private static void fileAddress(Map userLocaleMap,
240: InternetAddress address, Locale locale, int toOrCC) {
241: List[] toAndCC = (List[]) userLocaleMap.get(locale);
242: if (toAndCC == null) {
243: toAndCC = new List[2];
244: toAndCC[0] = new ArrayList();
245: toAndCC[1] = new ArrayList();
246: userLocaleMap.put(locale, toAndCC);
247: }
248: toAndCC[toOrCC].add(address);
249: }
250:
251: /**
252: * Checks if a user is the dummy user which indicates
253: * that an Email should be sent to the archive Email addresses
254: */
255: public static boolean isArchiveUser(ScarabUser user) {
256: return user.getUserId() == ARCHIVE_USER_ID;
257: }
258:
259: /**
260: * returns the dummy user which indicates
261: * that an Email should be sent to the archive Email addresses
262: */
263: public static ScarabUser getArchiveUser() throws TorqueException {
264: if (archiveUser == null) {
265: archiveUser = ScarabUserManager.getInstance();
266: archiveUser.setUserId(ARCHIVE_USER_ID);
267: }
268: return archiveUser;
269: }
270:
271: /**
272: * returns the archive Email addresses of a module
273: */
274: private static Set getArchiveAddresses(Module module) {
275: Set expandedArchiveAddresses = new HashSet();
276:
277: String archiveAddresses = module.getArchiveEmail();
278: if (archiveAddresses != null) {
279: StringTokenizer st = new StringTokenizer(archiveAddresses,
280: ",;");
281: while (st.hasMoreTokens())
282: expandedArchiveAddresses.add(st.nextToken().trim());
283: }
284: return expandedArchiveAddresses;
285: }
286:
287: private static void fileUser(Map userLocaleMap, ScarabUser user,
288: Module module, int toOrCC) throws Exception {
289: if (!isArchiveUser(user)) {
290: fileAddress(userLocaleMap, new InternetAddress(user
291: .getEmail(), user.getName()), chooseLocale(user,
292: module), toOrCC);
293: } else {
294: for (Iterator addresses = getArchiveAddresses(module)
295: .iterator(); addresses.hasNext();) {
296: fileAddress(userLocaleMap, new InternetAddress(
297: (String) addresses.next(), user.getName()),
298: chooseLocale(user, module), toOrCC);
299:
300: }
301: }
302: }
303:
304: /**
305: * Override the super.handleRequest() and process the template
306: * our own way.
307: * This could have been handled in a more simple way, which was
308: * to create a new service and associate the emails with a different
309: * file extension which would have prevented the need to override
310: * this method, however, that was discovered after the fact and it
311: * also seemed to be a bit more work to change the file extension.
312: */
313: protected String handleRequest() throws ServiceException {
314: String result = null;
315: try {
316: result = VelocityEmail.handleRequest(new ContextAdapter(
317: getContext()), getTemplate());
318: } catch (Exception e) {
319: throw new ServiceException(e); //EXCEPTION
320: }
321: return result;
322: }
323:
324: /**
325: * @param context The context in which to send mail, or
326: * <code>null</code> to create a new context.
327: * @param fromUser Can be any of the following: ScarabUser, two
328: * element String[] composed of name and address, base portion of
329: * the key used for a name and address property lookup.
330: * @param replyToUser Can be any of the following: ScarabUser, two
331: * element String[] composed of name and address, base portion of
332: * the key used for a name and address property lookup.
333: */
334: private static Email getEmail(EmailContext context, Module module,
335: Object fromUser, Object replyToUser, String template)
336: throws Exception {
337: Email te = new Email();
338: if (context == null) {
339: context = new EmailContext();
340: }
341: te.setContext(context);
342:
343: EmailLink el = EmailLinkFactory.getInstance(module);
344: context.setLinkTool(el);
345:
346: String[] nameAndAddr = getNameAndAddress(fromUser);
347: te.setFrom(nameAndAddr[0], nameAndAddr[1]);
348:
349: nameAndAddr = getNameAndAddress(replyToUser);
350: log.debug("Add from name[" + nameAndAddr[0] + "], address["
351: + nameAndAddr[1] + "]");
352: te.addReplyTo(nameAndAddr[0], nameAndAddr[1]);
353:
354: if (template == null) {
355: template = Turbine.getConfiguration().getString(
356: "scarab.email.default.template", "Default.vm");
357: }
358: log.debug("Add template [" + template + "]");
359: te.setTemplate(prependDir(template));
360:
361: String subjectTemplate = context.getSubjectTemplate();
362: if (subjectTemplate == null) {
363: int templateLength = template.length();
364: // The magic number 7 represents "Subject"
365: StringBuffer templateSB = new StringBuffer(
366: templateLength + 7);
367: // The magic number 3 represents ".vm"
368: templateSB
369: .append(template.substring(0, templateLength - 3));
370: subjectTemplate = templateSB.append("Subject.vm")
371: .toString();
372: }
373:
374: String subjectText = getSubject(context, subjectTemplate);
375: log.debug("Add subject [" + subjectText + "]");
376: te.setSubject(subjectText);
377: return te;
378: }
379:
380: /**
381: * Leverages the <code>fromName</code> and
382: * <code>fromAddress</code> properties when <code>input</code> is
383: * neither a <code>ScarabUser</code> nor <code>String[]</code>.
384: */
385: private static String[] getNameAndAddress(Object input) {
386: String[] nameAndAddr;
387: if (input instanceof ScarabUser) {
388: ScarabUser u = (ScarabUser) input;
389: nameAndAddr = new String[] { u.getName(), u.getEmail() };
390: } else if (input instanceof String[]) {
391: nameAndAddr = (String[]) input;
392: } else {
393: // Assume we want a property lookup, and the base portion
394: // of the key to use for that lookup was passed in.
395: String keyBase = (String) input;
396: if (keyBase == null) {
397: keyBase = "scarab.email.default";
398: }
399:
400: // TODO: Discover a better sending host/domain than
401: // "localhost"
402:
403: nameAndAddr = new String[2];
404: nameAndAddr[0] = Turbine.getConfiguration().getString(
405: keyBase + ".fromName", "Scarab System");
406: nameAndAddr[1] = Turbine.getConfiguration().getString(
407: keyBase + ".fromAddress", "help@localhost");
408: }
409: return nameAndAddr;
410: }
411:
412: private static String getSubject(TemplateContext context,
413: String template) {
414: template = prependDir(template);
415: String result = null;
416: try {
417: // render the template
418: result = VelocityEmail.handleRequest(new ContextAdapter(
419: context), template);
420: if (result != null) {
421: result = result.trim();
422: }
423: // in some of the more complicated templates, we set a context
424: // variable so that there is not a whole bunch of whitespace
425: // that can make it into the subject...
426: String subject = (String) context.get("emailSubject");
427: if (subject != null) {
428: result = subject.trim();
429: }
430: } catch (Exception e) {
431: log.error("Error rendering subject for " + template + ". ",
432: e);
433: result = "Scarab System Notification";
434: }
435: return result;
436: }
437:
438: private static String prependDir(String template) {
439: boolean b = false;
440: try {
441: b = GlobalParameterManager
442: .getBoolean(GlobalParameter.EMAIL_INCLUDE_ISSUE_DETAILS);
443: } catch (Exception e) {
444: log.debug("", e);
445: // use the basic email
446: }
447: return b ? "email/" + template : "basic_email/" + template;
448: }
449:
450: /**
451: * Returns a charset for the given locale that is generally
452: * preferred by email clients. If not specified by the property
453: * named by {@link
454: * org.tigris.scarab.util.ScarabConstants#DEFAULT_EMAIL_ENCODING_KEY},
455: * ask the <code>MimeTypeService</code> for a good value (except
456: * for Japanese, which always uses the encoding
457: * <code>ISO-2022-JP</code>).
458: *
459: * @param locale a <code>Locale</code> value
460: * @return a <code>String</code> value
461: */
462: public static String getCharset(Locale locale) {
463: String charset = Turbine.getConfiguration().getString(
464: ScarabConstants.DEFAULT_EMAIL_ENCODING_KEY, "").trim();
465: if (charset.length() == 0 || "native".equalsIgnoreCase(charset)) {
466: if ("ja".equals(locale.getLanguage())) {
467: charset = "ISO-2022-JP";
468: } else {
469: charset = ComponentLocator.getMimeTypeService()
470: .getCharSet(locale);
471: }
472: }
473:
474: return charset;
475: }
476:
477: private static Locale chooseLocale(ScarabUser user, Module module) {
478: Locale locale = null;
479: if (user != null) {
480: try {
481: locale = user.getPreferredLocale();
482: } catch (Exception e) {
483: log.error("Couldn't determine locale for user "
484: + user.getUserName(), e);
485: }
486: }
487: if (locale == null) {
488: if (module != null && module.getLocale() != null) {
489: locale = module.getLocale();
490: } else {
491: locale = ScarabConstants.DEFAULT_LOCALE;
492: }
493: }
494: return locale;
495: }
496: }
|