View Javadoc

1   /*
2    * Copyright (c) 2004-2007 Creative Sphere Limited.
3    * All rights reserved. This program and the accompanying materials
4    * are made available under the terms of the Eclipse Public License v1.0
5    * which accompanies this distribution, and is available at
6    * http://www.eclipse.org/legal/epl-v10.html
7    *
8    * Contributors:
9    *
10   *   Creative Sphere - initial API and implementation
11   *
12   */
13  package org.abstracthorizon.mercury.common;
14  
15  import java.io.File;
16  import java.io.FileInputStream;
17  import java.io.FileOutputStream;
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.util.ArrayList;
22  import java.util.Iterator;
23  import java.util.List;
24  import java.util.Properties;
25  
26  import javax.mail.Folder;
27  import javax.mail.MessagingException;
28  import javax.mail.NoSuchProviderException;
29  import javax.mail.PasswordAuthentication;
30  import javax.mail.Session;
31  import javax.mail.Store;
32  import javax.mail.URLName;
33  
34  import org.abstracthorizon.danube.support.RuntimeIOException;
35  import org.abstracthorizon.mercury.common.exception.UnknownUserException;
36  import org.abstracthorizon.mercury.common.exception.UserRejectedException;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  /**
41   * <p>
42   * This class is simple implementation of SMTPManager that uses
43   * properties file to read (and write) domain names to served,
44   * mail boxes that exist and aliases that point to existing mailboxes.
45   * </p>
46   * <p>
47   * Format of properties file is as described in {@link Properties} and
48   * this class uses special prefixes and property names to denote domain names, mailboxes
49   * and aliases:
50   * </p>
51   * <ul>
52   * <li><i>server.domain</i> - main domain name to appear in response to HELO/EHLO command</li>
53   * <li><i>-@domainname</i> - defines served domain. Main domain must appear in this form as well.
54   *                    Also this can point to a mailbox/alias (key's value) and then it will act as
55   *                    &quot;catch all&quot; account. In case value of this key is empty there
56   *                    is no catch all account.</li>
57   * <li><i>-mailbox@domain</i> - this is standard entry for mailbox/alias. Value of such key
58   *                              defines what does it represent:
59   *   <ul>
60   *     <li><i>S=</i> - a mail box. Rest of the string represents full URL as used to obtain
61   *                     JavaMail storage.
62   *     </li>
63   *     <il><i>A=</i> - an alias. It contains full e-mail address of a mailbox as defined before (not the URL).</li>
64   *   </ul>
65   *   Note: these two appear in a properties file like <code>&quot;S\=&quot</code> and <code>&quot;A\=&quot;</code>.
66   * </li>
67   * </ul>
68   *
69   *
70   * @author Daniel Sendula
71   */
72  public class SimpleStorageManager implements StorageManager {
73  
74      /** Mail box is case sensitive flag */
75      protected boolean caseSensitive = false;
76  
77      /** Properties to keep all elements in */
78      protected Properties props = new Properties();
79  
80      /** Maildir session to work  in */
81      protected Session session;
82  
83      /** Properties file */
84      protected File propertiesFile;
85  
86      /** Shell changes in properties automatically trigger saving values back to the file */
87      protected boolean autosave = true;
88  
89      /** Logger */
90      protected static final Logger logger = LoggerFactory.getLogger(SimpleStorageManager.class);
91  
92      /**
93       * Constructor
94       */
95      public SimpleStorageManager() {
96      }
97  
98      /**
99       * Setter for file
100      * @param propertiesFile file
101      */
102     public void setPropertiesFile(File propertiesFile) {
103         this.propertiesFile = propertiesFile;
104     }
105 
106     /**
107      * Returns properties file
108      * @return properties file
109      */
110     public File getPropertiesFile() {
111         return propertiesFile;
112     }
113 
114     /**
115      * Sets autosave flag
116      * @param autosave autosave flag
117      * @see #autosave
118      */
119     public void setAutosave(boolean autosave) {
120         this.autosave = autosave;
121     }
122 
123     /**
124      * Returns autosave flag
125      * @return autosave flag
126      * @see #autosave
127      */
128     public boolean isAutosave() {
129         return autosave;
130     }
131 
132     /**
133      * Sets case sensitive flag
134      * @param caseSensitive case sensitive
135      * @see #caseSensitive
136      */
137     public void setCaseSensitive(boolean caseSensitive) {
138         this.caseSensitive = caseSensitive;
139         if (!caseSensitive) {
140             toLowerCase();
141         }
142     }
143 
144     /**
145      * Returns case senstive
146      * @return case sensitive
147      * @see #caseSensitive
148      */
149     public boolean isCaseSensitive() {
150         return caseSensitive;
151     }
152 
153     /**
154      * Returns java mail session
155      * @return java mail session
156      */
157     public Session getJavaMailSession() {
158         return session;
159     }
160 
161     /**
162      * Sets java mail session
163      * @param session java mail session
164      */
165     public void setJavaMailSession(Session session) {
166         this.session = session;
167     }
168 
169 
170     /**
171      * This method loads properties and sets the session if not already defined
172      * @throws IOException
173      */
174     public void init() throws IOException {
175         load();
176         if (session == null) {
177             session = Session.getDefaultInstance(new Properties());
178         }
179     }
180 
181     /**
182      * This method loads the properties
183      * @throws IOException
184      */
185     public void load() throws IOException {
186         InputStream fis = getPropertiesInputStream();
187         try {
188             props.load(fis);
189         } finally {
190             fis.close();
191         }
192         if (!caseSensitive) {
193                 toLowerCase();
194         }
195     }
196 
197     /**
198      * This method saves the properties
199      * @throws IOException
200      */
201     public void save() {
202         try {
203             OutputStream fos = getPropertiesOutputStream();
204             if (fos != null) {
205                 try {
206                     props.store(fos, "# SMTP Manager data");
207                 } finally {
208                     fos.close();
209                 }
210             }
211         } catch (IOException e) {
212             throw new RuntimeIOException(e);
213         }
214     }
215 
216     /**
217      * Returns input stream to read properties from
218      * @return input stream to read properties from
219      * @throws IOException
220      */
221     protected InputStream getPropertiesInputStream() throws IOException {
222         return new FileInputStream(propertiesFile);
223     }
224 
225     /**
226      * Returns output stream to write properties to
227      * @return output stream to write properties to
228      * @throws IOException
229      */
230     protected OutputStream getPropertiesOutputStream() throws IOException {
231         return new FileOutputStream(propertiesFile);
232     }
233 
234     /**
235      * Returns main domain as it appears in HELO/EHLO command response
236      * @return main domain
237      */
238     public String getMainDomain() {
239         return props.getProperty("server.domain");
240     }
241 
242     /**
243      * Sets main domain name
244      * @param domain main domain name
245      */
246     public void setMainDomain(String domain) {
247         props.setProperty("server.domain", domain);
248     }
249 
250     /**
251      * Returns <code>true</code> if domain is served by this server
252      * @param <code>true</code> if domain is served by this server
253      */
254     public boolean hasDomain(String domain) {
255         if (!caseSensitive) {
256             domain = domain.toLowerCase();
257         }
258         if (domain.equals(getMainDomain())) {
259             return true;
260         }
261         return props.getProperty("-@" + domain) != null;
262     }
263 
264 
265     public Store findStore(String mailbox, String domain, char[] password) throws UserRejectedException, MessagingException {
266 
267         Folder folder = findInbox(mailbox, domain, password);
268         return folder.getStore();
269     }
270 
271     /**
272      * Updates path parameter with local mailbox (folder from JavaMail if mailbox/alias exists.
273      * @param mailbox mailbox to be accesses
274      * @param domain domain
275      * @param domain domain where mailbox is supposed ot be defined.
276      * @throws UserRejectedException thrown in user is rejected
277      * @throws MessagingException if error happens while accessing the folder
278      */
279     public Folder findInbox(String mailbox, String domain, char[] password) throws UserRejectedException, MessagingException {
280         if (domain == null) {
281             domain = getMainDomain();
282         }
283         if (!caseSensitive) {
284             mailbox = mailbox.toLowerCase();
285             if (domain != null) {
286                 domain = domain.toLowerCase();
287             }
288         }
289 
290         String storeString = obtainStoreString(mailbox, domain);
291         if (storeString != null) {
292             URLName urlName = null;
293             if (password != null) {
294                 urlName = createURLName(mailbox, new String(password), storeString);
295             } else {
296                 urlName = createURLName(mailbox, null, storeString);
297             }
298 
299             try {
300                 if (password != null) {
301                     PasswordAuthentication auth = new PasswordAuthentication(mailbox, new String(password));
302                     session.setPasswordAuthentication(urlName, auth);
303                 }
304                 Store store = obtainStore(urlName);
305 
306                 store.connect();
307 
308                 String ref = urlName.getRef();
309                 if (ref != null) {
310                     return store.getFolder(ref);
311                 } else {
312                     return store.getFolder("INBOX");
313                 }
314             } catch (NoSuchProviderException e) {
315                 // try for catch all, but only in case that mail wasn't directed or redirected to postmaster and
316                 // that postmaster exists on final domain after resolving aliases
317                 if (!mailbox.equals("postmaster") && (props.getProperty("-" + makeEntry("postmaster", domain)) != null)) {
318                     return findInbox("postmaster", domain, null);
319                 } else {
320                     throw new UnknownUserException("User: " + makeEntry(mailbox, domain) + " now known", e);
321                 }
322             }
323         }
324 
325         throw new UnknownUserException("User: " + makeEntry(mailbox, domain) + " now known");
326     }
327 
328 //    protected String findAlias(String mailbox, String domain) throws MessagingException {
329 //
330 //        String storeString = null;
331 //        String mb = mailbox;
332 //        String dn = domain;
333 //        if (dn == null) {
334 //            dn = "";
335 //        }
336 //
337 //        String map = props.getProperty("-" + makeEntry(mb, dn));
338 //        if (map == null) {
339 //            map = props.getProperty("-" + makeEntry("", dn));
340 //        }
341 //        while ((map != null) && (storeString == null)) {
342 //            if (map.startsWith("S=")) {
343 //                storeString = map.substring(2);
344 //            } else if (map.startsWith("A=")) {
345 //                int i = map.indexOf('@');
346 //                if (i >= 0) {
347 //                    String t = map.substring(2, i);
348 //                    if (t.length() > 0) {
349 //                        mb = t;
350 //                    }
351 //                    dn = map.substring(i+1);
352 //                } else {
353 //                    throw new MessagingException("Configuration error; expecting '@' in map='" + map + "'");
354 //                }
355 //            } else{
356 //                mb = "";
357 //                dn = "";
358 //            }
359 //
360 //            map = props.getProperty("-" + makeEntry(mb, dn));
361 //            if (map == null) {
362 //                map = props.getProperty("-" + makeEntry("", dn));
363 //            }
364 //        }
365 //        return storeString;
366 //    }
367 
368     protected String obtainStoreString(String mailbox, String domain) throws MessagingException {
369 
370         String storeString = null;
371         String mb = mailbox;
372         String dn = domain;
373         if (dn == null) {
374             dn = "";
375         }
376 
377         String map = props.getProperty("-" + makeEntry(mb, dn));
378         if (map == null) {
379             map = props.getProperty("-" + makeEntry("", dn));
380         }
381         while ((map != null) && (storeString == null)) {
382             if (map.startsWith("S=")) {
383                 storeString = map.substring(2);
384                 storeString = decorateStoreString(mb, dn, storeString);
385             } else if (map.startsWith("A=")) {
386                 int i = map.indexOf('@');
387                 if (i >= 0) {
388                     String t = map.substring(2, i);
389                     if (t.length() > 0) {
390                         mb = t;
391                     }
392                     dn = map.substring(i+1);
393                 } else {
394                     throw new MessagingException("Configuration error; expecting '@' in map='" + map + "'");
395                 }
396             } else{
397                 mb = "";
398                 dn = "";
399             }
400 
401             map = props.getProperty("-" + makeEntry(mb, dn));
402             if (map == null) {
403                 map = props.getProperty("-" + makeEntry("", dn));
404             }
405         }
406         return storeString;
407     }
408 
409     protected String decorateStoreString(String mailbox, String domain, String storeString) {
410         return storeString;
411     }
412 
413     protected Store obtainStore(URLName urlName) throws MessagingException {
414         // TODO Replacing alias with actual store
415         //          urlName = new URLName(urlName.getProtocol(), urlName.getHost(), urlName.getPort(), urlName.getFile(), mb, null);
416 
417           Store store = session.getStore(urlName);
418           return store;
419     }
420 
421     protected URLName createURLName(String username, String password, String storeString) {
422         URLName urlName = new URLName(storeString);
423 
424         urlName = new URLName(urlName.getProtocol(), urlName.getHost(), urlName.getPort(), urlName.getRef() == null ? urlName.getFile() : urlName.getFile() + "#" + urlName.getRef(), username, password);
425         return urlName;
426     }
427 
428     /**
429      * Utility method to add new mailbox to this manager. It saves properties if {@link #autosave} is on.
430      *
431      * @param mailbox mailbox (name@domain).
432      * @param store url
433      * @throws IOException
434      */
435     public void addMailbox(String mailbox, String store) {
436         if (!caseSensitive) {
437             mailbox = mailbox.toLowerCase();
438         }
439         props.setProperty("-" + mailbox, "S=" + store);
440         if (autosave) { save(); }
441     }
442 
443     /**
444      * Utility method that adds new alias to this manager. It saves properties if {@link #autosave} is on.
445      * @param mailbox mailbox (name@domain)
446      * @param destMailbox destination mailbox (name@domain)
447      * @throws IOException
448      */
449     public void addAlias(String mailbox, String destMailbox) throws IOException {
450         if (!caseSensitive) {
451             mailbox = mailbox.toLowerCase();
452         }
453         props.setProperty("-"+mailbox, "A="+destMailbox);
454         if (autosave) { save(); }
455     }
456 
457     /**
458      * Removes the alias. It won't remove anything else.
459      * @param mailbox removes the alias
460      */
461     public void removeAlias(String mailbox) {
462         if (!caseSensitive) {
463             mailbox = mailbox.toLowerCase();
464         }
465         String property = props.getProperty("-" + mailbox);
466         if ((property != null) && property.startsWith("A=")) {
467             props.remove("-" + mailbox);
468         }
469     }
470 
471     /**
472      * Adds new domain to this manager. It saves properties if {@link #autosave} is on.
473      * @param domain domain name.
474      * @throws IOException
475      */
476     public void addDomain(String domain) {
477         if (!caseSensitive) {
478             domain = domain.toLowerCase();
479         }
480         if (props.getProperty("-" + makeEntry("", domain)) == null) {
481             props.setProperty("-" + makeEntry("", domain), "R");
482             if (autosave) { save(); }
483         }
484     }
485 
486     /**
487      * Adds new &quot;raw&quot; entry to properties.
488      *
489      * @param mailbox key that is automatically prefixed with &quot;-&quot;
490      * @param entry entry
491      * @throws IOException
492      */
493     public void addEntry(String mailbox, String entry) throws IOException {
494         if (!caseSensitive) {
495             mailbox = mailbox.toLowerCase();
496         }
497         props.setProperty("-"+mailbox, entry);
498         if (autosave) { save(); }
499     }
500 
501     /**
502      * Removes mailbox
503      * @param mailbox mailbox
504      * @throws IOException
505      */
506     public boolean removeMailbox(String mailbox, String domain) {
507         boolean res = (props.remove("-" + makeEntry(mailbox, domain)) != null);
508 
509         if (autosave) { save(); }
510         return res;
511     }
512 
513     /**
514      * Deletes domain
515      * @param domain domain name
516      * @throws IOException
517      */
518     public boolean removeDomain(String domain) {
519         boolean res = (props.remove("-" + makeEntry("", domain)) != null);
520         if (autosave) { save(); }
521         return res;
522     }
523 
524     /**
525      * Returns all domains that are served
526      * @return all domains that are served
527      */
528     public String[] getDomains() {
529         List<String> list = new ArrayList<String>();
530         Iterator<Object> it = props.keySet().iterator();
531         while (it.hasNext()) {
532             String s = (String)it.next();
533             if (s.startsWith("-@")) {
534                 list.add(s.substring(2));
535             }
536         }
537         String[] res = new String[list.size()];
538         return list.toArray(res);
539     }
540 
541     /**
542      * Returns all mailbox urls
543      * @return mailbox urls
544      */
545     public String[] getMailboxNames() {
546         List<String> list = new ArrayList<String>();
547         Iterator<Object> it = props.keySet().iterator();
548         while (it.hasNext()) {
549             String s = (String)it.next();
550             if (s.startsWith("-")) {
551                 String e = props.getProperty(s);
552                 if (e.startsWith("S=")) {
553                     int i = s.indexOf('@');
554                     if (i >= 1) {
555                         s = s.substring(1, i);
556                     } else {
557                         s = s.substring(1);
558                     }
559                     list.add(s);
560                 }
561             }
562         }
563         String[] res = new String[list.size()];
564         return list.toArray(res);
565     }
566 
567     /**
568      * Returns all mailbox urls
569      * @param domain domain name
570      * @return mailbox urls
571      */
572     public String[] getMailboxNames(String domain) {
573         List<String> list = new ArrayList<String>();
574         Iterator<Object> it = props.keySet().iterator();
575         while (it.hasNext()) {
576             String s = (String)it.next();
577             if (s.startsWith("-")) {
578                 String e = props.getProperty(s);
579                 if (e.startsWith("S=")) {
580                     String d = "";
581                     int i = s.indexOf('@');
582                     if (i >= 1) {
583                         d = s.substring(i+1);
584                         s = s.substring(1, i);
585                     } else {
586                         s = s.substring(1);
587                     }
588                     if (domain.equals(d)) {
589                         list.add(s);
590                     }
591                 }
592             }
593         }
594         String[] res = new String[list.size()];
595         return list.toArray(res);
596     }
597 
598     /**
599      * Returns all aliases
600      * @return all aliases
601      */
602     public String[] getAliases() {
603         List<String> list = new ArrayList<String>();
604         Iterator<Object> it = props.keySet().iterator();
605         while (it.hasNext()) {
606             String s = (String)it.next();
607             if (s.startsWith("-")) {
608                 String e = props.getProperty(s);
609                 if (e.startsWith("A=")) {
610                     list.add(s.substring(1)+"="+e.substring(2));
611                 }
612             }
613         }
614         String[] res = new String[list.size()];
615         return list.toArray(res);
616     }
617 
618     /**
619      * Converts all entries to lower case
620      */
621     protected void toLowerCase() {
622         ArrayList<Entry> changes = new ArrayList<Entry>();
623         Iterator<Object> it = props.keySet().iterator();
624         while (it.hasNext()) {
625             String key = (String)it.next();
626             String lowerCaseKey = key.toLowerCase();
627             if (!lowerCaseKey.equals(key) && (props.getProperty(lowerCaseKey) == null)) {
628                     String value = props.getProperty(key);
629                     it.remove();
630                     changes.add(new Entry(lowerCaseKey, value));
631             }
632         }
633         Iterator<Entry> it2 = changes.iterator();
634         while (it2.hasNext()) {
635             Entry entry = it2.next();
636             props.setProperty(entry.key, entry.value);
637         }
638     }
639 
640     protected String makeEntry(String mailbox, String domain) {
641         if (domain != null) {
642             return mailbox + "@" + domain;
643         } else {
644             return mailbox + "@";
645         }
646     }
647 
648     /**
649      * Entry
650      */
651     protected static class Entry {
652         protected String key;
653         protected String value;
654         public Entry(String key, String value) {
655             this.key = key;
656             this.value = value;
657         }
658     }
659 
660     /**
661      * Fix for url name
662      */
663     protected static class URLNameFix extends URLName {
664 
665         /**
666          * Constructor
667          * @param urlName url name
668          * @param username username
669          */
670         public URLNameFix(URLName urlName, String username) {
671             super(urlName.getProtocol(), urlName.getHost(), urlName.getPort(), urlName.getRef() == null ? urlName.getFile() : urlName.getFile() + "#" + urlName.getRef(), username, null);
672         }
673 
674         /**
675          * Returns fixed filename of URL name (adding ref part
676          * @return file
677          */
678         public String getFile() {
679             String file = super.getFile();
680             String ref = super.getRef();
681             if (ref == null) {
682                 return file;
683             } else {
684                 return file + "#" + ref;
685             }
686         }
687 
688     }
689 }