001package Torello.Java;
002
003import java.io.*;
004import java.util.*;
005import javax.mail.internet.*;
006
007/**
008 * Some simple methods that make use of the Java Mail API.
009 * 
010 * <BR /><BR /><EMBED CLASS="external-html" DATA-FILE-ID="EML">
011 */
012public class EMailLists extends TreeMap<String, String[]>
013{
014    /** <EMBED CLASS="external-html" DATA-FILE-ID="SVUID">  */
015    public static final long serialVersionUID = 1;
016
017    /**
018     * This stores the name of the data-file that contains the Rolodex information.
019     * It is a <B><I>compressed</I></B> (ZIP-format Compression), java
020     * <B><I>java.io.serializable</I></B> object file containing just a single
021     * data object of type: "{@code java.util.TreeMap<String, String[]>}"
022     */
023    public final String DATA_FILE_NAME;
024
025    /**
026     * This simply takes the file name that has been provided via the {@code String diskFileName}
027     * parameter, and loads that file to memory from disk.
028     *
029     * @param dataFileName The name of a datafile that has been stored somewhere on disk.  It is
030     * stored as a {@code java interface java.io.Serializable} object file.  <B><I>The file that is
031     * on disk will not be of type {@code EMailLists} but rather of type
032     * {@code java.util.TreeMap<String, String[]>}, to ensure forward-compatability</I></B>.  This
033     * conversion is done automatically within this class by the {@code public void writeToDisk()}
034     * method.
035     *
036     * @return An instance of {@code class EMailLists} that has been fully instantiated and
037     * populated with the Rolodex data that is stored within the data-file identified by the
038     * data-file name provided in the parameter list.
039     */
040    public static EMailLists loadFromDisk(String dataFileName) throws IOException
041    {
042        @SuppressWarnings("unchecked")
043        TreeMap<String, String[]> tm = (TreeMap<String, String[]>) FileRW.readObjectFromFileNOCNFE
044            (dataFileName, TreeMap.class, true);
045
046        return new EMailLists(dataFileName, tm, false);
047    }
048
049    /**
050     * Loads an instance of this class from a {@code TreeMap<String, String[]>} - which may
051     * or may or may not have been generated by this class.
052     *
053     * @param tm A map of e-mail address lists.
054     *
055     * @return An instance of {@code class EMailLists} that has been fully instantiated and
056     * populated with the Rolodex data that is stored within the data-file identified by the
057     * data-file name provided in the parameter list.
058     */
059    public static EMailLists fromTreeMap(TreeMap<String, String[]> tm)
060    { return new EMailLists(null, tm, false); }
061
062    /**
063     * Loads this Rolodex from a MIME-{@code String}
064     *
065     * @param b64MimeEncoded The internal e-mail address {@code TreeMap} data, encoded as a 
066     * base 64 MIME {@code String}.
067     *
068     * @return An instance of this class, derived from a {@code MIME-String} encoded data.
069     */
070    @SuppressWarnings("unchecked")
071    public static EMailLists fromString(String b64MimeEncoded) throws IOException
072    { return new EMailLists(null, (TreeMap<String, String[]>) StringParse.b64MimeStrToObj(b64MimeEncoded), false); }
073
074    /**
075     * This creates a brand new, empty "e-mail address Rolodex."  This class is a sub-class of 
076     * {@code java.util.TreeMap<String, String[]>}
077     *
078     * @return A new instance of this class.
079     *
080     * <BR /><B>IMPORTANT NOTE:</B> A data-file for this class instance will not be created.
081     * After saving some lists to this Rolodex, the user must execute a call to: <B>{@code public
082     * void writeToDisk()}</B> in order to check the data in this data-structure out to disk.
083     */
084    public static EMailLists createNewInstance()
085    { return new EMailLists(null, null, true); }
086
087
088    /**
089     * This loads a data-file that must have been generated by the <B>{@code interface
090     * java.io.Serializable}</B>.  The contents of this data file <B><I>should be just one java
091     * {@code java.lang.Object} of specific-type {@code java.util.TreeMap<String, String[]>}</I></B>
092     *
093     * @param dataFileName The name of the file where data will or should be stored.
094     *
095     * @param tm A {@code TreeMap} containing an e-mail address Rolodex.
096     *
097     * @param newInstance This should be set to <B>TRUE</B> if the user intends to create a
098     * <I>brand new instance</I> of the EMailLists class.
099     *
100     * <BR /><BR /><B>NOTE:</B> If this variable is true, the disk-data file specified by
101     * parameter {@code dataFileName} will not be visited.
102     *
103     * <BR /><BR /><B>CAUTION:</B> If this variable is set to <B>TRUE</B>, but there is already an
104     * older data-file present on disk with this file-name, then <I>that file will be
105     * over-written</I> with the first call the programmer makes to
106     * {@code public void writeToDisk()}}.
107     */
108    protected EMailLists
109        (String dataFileName, TreeMap<String, String[]> tm, boolean newInstance)
110    {
111        super();
112
113        this.DATA_FILE_NAME = dataFileName;
114
115        if (! newInstance) this.putAll(tm);
116    }
117
118
119    /**
120     * This saves the data-contents of this class directly to disk.  It uses the
121     * {@code interface java.io.Serializable} to save the data.
122     *
123     * <BR /><BR /><B>NOTE:</B> The data stored in this
124     * <B>{@code class TreeMap<String, String[]>}</B>, which is indeed a parent-class that is
125     * "extended" by this (the <B>{@code class EMailLists}</B>), is first converted back into the
126     * parent TreeMap class.  This is done for one reason, and that is regarding Java's quirks in
127     * {@code Object} serialization.  If future versions of {@code class EMailLists} are written,
128     * previously serialized objects of type {@code Torello.Java.EMailLists} would cease to be
129     * readable with the serializable interface.  In the method {@code public void writeToDisk()},
130     * the contents of the Tree-Map are cloned into the parent TreeMap class, and only then are
131     * they written to disk.
132     */
133    public void writeToDisk() throws IOException
134    {
135        if (DATA_FILE_NAME == null) throw new IllegalArgumentException(
136            "This instance of EMailLists was not loaded from disk, so there is no data file-name " +
137            "to write the TreeMap to."
138        );
139
140        TreeMap<String, String[]> temp = new TreeMap<>();
141
142        temp.putAll(this);
143
144        FileRW.writeObjectToFileNOCNFE(this, DATA_FILE_NAME, true);
145    }
146
147    /**
148     * Writes this class' internal e-mail address data to disk.
149     * 
150     * @param fileName The name of the data-file to be used for writing.
151     */
152    public void writeToDisk(String fileName) throws IOException
153    {
154        TreeMap<String, String[]> temp = new TreeMap<>();
155
156        temp.putAll(this);
157
158        FileRW.writeObjectToFileNOCNFE(this, fileName, true);
159    }
160
161    /**
162     * Writes this class' internal e-mail address data to an output {@code String}.
163     *
164     * @return This class' internal data as a Base-64 MIME Encoded {@code String}.
165     */
166    public String writeToString() throws IOException
167    {
168        TreeMap<String, String[]> temp = new TreeMap<>();
169
170        temp.putAll(this);
171
172        return StringParse.objToB64MimeStr(temp);
173    }
174
175    /**
176     * This This prints the names of the lists to a {@code StringBuilder}, and how many e-mail
177     * addresses are in each list.
178     */
179    public String summariesToString() throws IOException
180    {
181        StringBuilder sb = new StringBuilder();
182
183        for (String s : keySet())
184            sb.append(StringParse.zeroPad(get(s).length) + " elements in group: " + s);
185
186        return sb.toString();
187    }
188
189    /**
190     * This simply prints the contents of the underlying {@code TreeMap} data-structure using a
191     * {@code StringBuilder}, and returns the {@code String}.
192     * @return A String representation of the internal {@code TreeMap}
193     */
194    public String listsToString()
195    {
196        StringBuilder sb = new StringBuilder();
197
198        for (String listName : keySet())
199        {
200            String[] list = get(listName);
201
202            sb.append(
203                "************************************************************************************************************\n" +
204                "PRINTING LIST:\t" + listName + ", which has " + list.length + " elements.\n"
205            );
206
207            int count = 1;
208            for (String eMailAddr : list) sb.append("[" + (count++) + ": " + eMailAddr + "], ");
209
210            sb.append("\nThere were a total of " + (count-1) + " addresses in this subset.\n");
211        }
212
213        return sb.toString();
214    }
215
216
217    /**
218     * This generates a simple, view-able, HTML file as a {@code String}.  The HTML
219     * {@code String} that is returned does not contain header information, nor footer info.  It is
220     * just a series of {@code <H1> List Name </H1>} followed by tables as
221     * {@code <TABLE CLASS="EMailListsTable"> ... </TABLE>}.  The user needs to add opening and
222     * closing HTML tags if this is to be a complete page with well-formatted HTML.
223     *
224     * @return The internal e-mail Rolodex data-structure as a series of HTML tables.
225     */
226    public String printToHTML() throws IOException
227    {
228        StringBuilder sb = new StringBuilder();
229        sb.append("<STYLE type=\"text/css\">\n.EMailListsTable TD { width: 33%; }\n</STYLE>\n");
230
231        for (String listName : keySet())
232        {
233            String[] list = get(listName);
234            sb.append("<H1>" + listName + "</H1>\n<TABLE CLASS=\"EMailListsTable\" STYLE=\"width: 100%;\">");
235
236            for (int i=0; i < list.length; i += 3)
237                sb.append(
238                    "<TR>\n" +
239                    "<TD>" + list[i] + "</TD>\n" +
240                    "<TD>" + (i+1 < list.length ? list[i+1] : " ") + "</TD>\n" +
241                    "<TD>" + (i+2 < list.length ? list[i+2] : " ") + "</TD>\n" +
242                    "</TR>\n"
243                );
244
245            sb.append("</TABLE>\n<BR /><BR />\n\n\n");
246        }
247        return sb.toString();
248    }
249
250
251    /**
252     * This does a simple validity check by attempting to instantiate each e-mail address stored in
253     * the {@code String[]} e-mail address {@code String Arrays} within the TreeMap by performing
254     * this instantiation: {@code new javax.mail.internet.InternetAddress(eMailAddress); }
255     *
256     * <BR /><BR />If the above line of code throws an Exception, then this will be identified as
257     * an "invalid e-mail address" and returned with the result set.  <B><I>Invalid e-mail
258     * addresses are not removed from the data-structure</I></B>, but rather, they are returned
259     * with the result set.
260     *
261     * <BR /><BR />If the parameter {@code boolean verbosePrint} is set to TRUE, messages will be
262     * sent to {@code System.out.println(...)}
263     *
264     * @param listName The name of the list whose {@code String[] Array} contents need to be
265     * checked here.
266     *
267     * @param verbosePrint When <B>TRUE</B>, messages will be sent to {@code System.out}
268     *
269     * @return A list of potentially improperly formed e-mail addresses.
270     */
271    public Vector<String> checkValidity(String listName, boolean verbosePrint)
272    {
273        Vector<String> ret = new Vector<String>();
274
275        for (String emailAddress : get(listName))
276            try
277                { InternetAddress ia = new InternetAddress(emailAddress); System.out.print("."); }
278            catch (Exception e)
279            {
280                if (verbosePrint) System.out.println(
281                    "\nE-Mail Address:\t[" + emailAddress + "] from list:\t[" + listName + "] " +
282                    "generated an Exception!"
283                );
284
285                ret.addElement(emailAddress);
286            }
287
288        return ret;
289    }
290
291
292    /**
293     * This performs the validity check on each and every list in this {@code TreeMap}
294     * data-structure.  It calls the method {@link #checkValidity(String, boolean)}
295     *
296     * @param verbosePrint When <B>TRUE</B>, messages will be sent to {@code System.out}
297     *
298     * @return A list of potentially improperly formed e-mail addresses.
299     *
300     * @see #checkValidity(String, boolean)
301     */
302    public Vector<String> checkValidity(boolean verbosePrint) throws IOException
303    {
304        Vector<String> ret = new Vector<String>();
305
306        for (String listName : keySet())
307        {
308            if (verbosePrint) System.out.print("Working list: " + listName);
309
310            ret.addAll(checkValidity(listName, verbosePrint));
311
312            if (verbosePrint) System.out.println();
313        }
314
315        return ret;
316    }
317
318
319    /**
320     * <B>NOTE:</B> The original name of this function was
321     * {@code removePossibleDuplicatesAndSort(String)}, but was shortened to just "{@code clean}"
322     *
323     * <BR /><BR />This will go through the contents of the list identified by {@code 'listName'},
324     * and ensure there are no duplicate e-mail addresses.  If there are, they will be removed.
325     * The arrays are also sorted.  Each address stored in the {@code String Arrays} is converted
326     * to lower-case text, and trimmed.
327     *
328     * <BR /><BR /><B>NOTE:</B> If this list has been modified, it is up to the user/programmer to
329     * write the changes to the disk data-file.  Disk data-file writes are <B><I>not done
330     * automatically</I></B>.  You need to concern yourself with this aspect.
331     *
332     * @param listName The name of the {@code String[] list} in the underlying
333     * {@code java.util.TreeMap<String, String[]>} data-structure whose contents need to be pruned.
334     *
335     * @return The number of e-mail addresses that were removed from this subset of the Rolodex,
336     * because there were duplicates present.
337     */
338    public int clean(String listName)
339    {
340        String[]        list        = get(listName);
341        TreeSet<String> ts          = new TreeSet<String>();
342        int             oldLength   = list.length;
343
344        for (int i=0; i < oldLength; i++) ts.add(list[i].toLowerCase().trim());
345
346        put(listName, ts.toArray(new String[0]));
347
348        return oldLength - ts.size();
349    }
350
351
352    /**
353     * The original name of this function was {@code removePossibleDuplicatesAndSort(String)},
354     * but was shortened to just {@code 'cleanEachList()'}. This will remove all duplicates from
355     * each and every list in the {@code TreeMap}.  The arrays will also be sorted.  Each address
356     * stored in the {@code String[] array's} is converted to lower-case text, and trimmed.
357     *
358     * <BR /><B>IMPORTANT NOTE:</B> This will removed duplicates that are <B><I>identified on a on
359     * a "list by list" basis.</I></B>  This means that in the case where there are two identical
360     * e-mail addresses in a single list, one will be removed.  <B><I>HOWEVER</I></B>, if a single
361     * e-mail address is present in multiple, different lists, this method will not catch that
362     * scenario as a mistake.  If the programmer considers this scenario a mistake, it should be
363     * important to remember that the underlying {@code TreeMap<String, String[]>} data-structure
364     * is perfectly visible (as a super-class), and therefore can be modified in whatever way
365     * 3rd-party programmers wish to change the contents of this data-structure.
366     * 
367     * <BR /><BR />The clean methods in this class are just designed to make sure that the arrays
368     * themselves do not have multiple copies of the same address, because that is a guaranteed
369     * "programmatically incorrect" scenario.  Having a single address distributed in multiple
370     * lists, however, might easily a desired situation for some programmers.
371     * 
372     * <BR /><BR />This is why the method is called "cleanEachList" instead of just "clean" or
373     * "cleanAll" - since they are done sequentially.
374     *
375     * @return The total number of e-mail addresses that were duplicates and were removed from all
376     * lists in this Rolodex.
377     */
378    public int cleanEachList()
379    {
380        int total = 0;
381
382        for (String listName : keySet()) total += clean(listName);
383
384        return total;
385    }
386
387
388    /**
389     * This will add the given e-mail address(es) to the specified list in this data structure.
390     * The list that is stored back into the {@code String[] Array} will be sorted, and the
391     * {@code String} elements will be made to lower-case text, and trimmed (using
392     * {@code java.lang.String.trim()}).
393     *
394     * @param listName The name of the <I>already-existing list</I> in this {@code TreeMap}.
395     *
396     * @param eMailAddress One or more e-mail addresses to be added to this list
397     *
398     * @throws IllegalArgumentException This will throw an exception if:
399     *
400     * <BR /><BR /><UL CLASS="JDUL">
401     * <LI> This {@code String[] Array} specified by parameter {@code 'listName'} in the
402     *      {@code TreeMap} already contains a copy of this e-mail address
403     * </LI>
404     * <LI> The passed parameter {@code String[] eMailAddress} has multiple copies of the same
405     *      address
406     * </LI>
407     * <LI> The instantiation {@code new javax.mail.internet.InternetAddress(eMailAddress)}
408     *      generates an exception
409     * </LI>
410     * </UL>
411     */
412    public void addAddress(String listName, String... eMailAddress)
413    {
414        TreeSet<String> ts      = new TreeSet<String>();
415        String[]        curList = get(listName);
416
417        for (String address : curList) ts.add(address.trim().toLowerCase());
418
419        for (String address : eMailAddress)
420        {
421            try
422                { new InternetAddress(address = address.trim().toLowerCase()); }
423            catch (Exception e)
424            {
425                throw new IllegalArgumentException(
426                    "E-Mail Address: [" + address + "] is not a valid address.  It generated an " +
427                    "exception:\n" + e.getMessage()
428                );
429            }
430
431            if (! ts.add(address)) throw new IllegalArgumentException
432                ("E-Mail Address: [" + address + "] is already in the list: [" + listName + "]");
433        }
434
435        put(listName, ts.toArray(new String[0]));
436    }
437
438    /**
439     * Removes the specified e-mail addresses from the list specified by the
440     * {@code String listName} parameter.
441     *
442     * @param listName The name of the <I>already-existing list</I> in this {@code TreeMap}.
443     *
444     * @param eMailAddress One or more e-mail addresses to be added to this {@code String[] Array}
445     * e-mail address list.
446     *
447     * @throws IllegalArgumentException This will throw an exception if one or more of the
448     * requested addresses to be removed <I><B>is/are not actually present in the underlying
449     * {@code String[] Array}</I></B> associated with {@code listName}.
450     */
451    public void removeAddress(String listName, String... eMailAddress)
452    {
453        TreeSet<String> ts      = new TreeSet<String>();
454        String[]        curList = get(listName);
455
456        for (String address : curList) ts.add(address.trim().toLowerCase());
457
458        for (String address : eMailAddress)
459            if (! ts.remove(address = address.trim().toLowerCase()))
460                throw new IllegalArgumentException
461                    ("E-Mail Address: [" + address + "] is not in the list: [" + listName + "]");
462
463        put(listName, ts.toArray(new String[0]));
464    }
465
466    /**
467     * This will create a new section in the Rolodex with the name of this list.  If the Rolodex
468     * already contains a list by this name, the two lists will be merged.
469     *
470     * @param listName This is the name "Rolodex subsection."  It is an element in the
471     * {@code TreeMap}, and contains one list of e-mail addresses.
472     *
473     * @param newAddressListFileName This is a list of e-mail addresses, stored in a text-file.
474     * The name of the text-file to load may be provided here.  The format of the file expects
475     * new addresses to be included, one per line.
476     *
477     * @return Whether updating a list that already exists, or creating a brand new list - the
478     * e-mail addresses that are found in the text file ({@code 'newAddressListFileName'}) will
479     * be checked for validity.  Checking for validity simply means calling Java's {@code class
480     * InternetAddress} constructor, and waiting to see if the address may be instantiated, or if
481     * it generates an exception.  If an exception does occur, that address will be skipped, and
482     * the exception will be saved in the response {@code TreeMap}.
483     *
484     * <BR /><BR />The response tree-map simply contains a list of the e-mail addresses that were
485     * problems to instantiate with the {@code class javax.mail.internet.InternetAddress}
486     * <I><B>in the TreeMap key field</B></I>.  In the lookup/value field of the {@code TreeMap},
487     * the exception that was generated will be provided.
488     */
489    public TreeMap<String, Exception> updateList
490        (String listName, String newAddressListFileName) throws IOException
491    {
492        TreeSet<String>             ts                  = new TreeSet<String>();
493        String[]                    curAddrList         = get(listName);
494        TreeMap<String, Exception>  errors              = new TreeMap<>();
495        Vector<String>              newAddrList         = FileRW.loadFileToVector(newAddressListFileName, false);
496
497        // Make sure that all the elements in the old list (if there is one!) are trimmed and lower-case
498        if (curAddrList != null)
499            for (String addr : curAddrList) ts.add(addr.trim().toLowerCase());
500
501        // Make sure all the newly loaded e-mail addresses are trim/lower-case
502        newAddrList.replaceAll(addr -> addr.trim().toLowerCase());
503
504        // Make sure that each new address does not cause an exception when instantiating.
505        for (String addr : newAddrList)
506            try                 { new InternetAddress(addr.trim().toLowerCase()); }
507            catch (Exception e) { errors.put(addr, e); }
508
509        // Add only the "safe addresses" to this list.
510        for (String addr : newAddrList) if (! errors.containsKey(addr)) ts.add(addr);
511
512        // Convert the new list to an array
513        String[]                    newAddrListAsArr    = new String[ts.size()];
514        int                         i                   = 0;
515
516        for (String addr : ts) newAddrListAsArr[i++] = addr;
517        put(listName, newAddrListAsArr);
518
519        if (errors.size() > 0) return errors; else return null;
520    }
521
522    /**
523     * Facilitates a (very) small console-driven menu for adding and removing addresses to specific
524     * lists.  At the BASH/UNIX/MS-DOS console, execute a call to:
525     * {@code java Torello.Java.EMailLists } to see this {@code printMenu()} help menu command
526     * output.  Follow instructions from there.
527     *
528     * <BR /><BR /><B>NOTE:</B> Menu Option #1 will ask you to specify the name of your e-mail
529     * list data-file.  It will not create one for your, but rather, just save that file-name to
530     * a temporary text-file in your current working directory.  This will be used for the duration
531     * of your console session.
532     */
533    public static void printMenu()
534    {
535        System.out.println(
536            "1: set temporary text-file pointer  ... SPECIFICALLY: argv[0]=\"1\", argv[1]=your-email-data-file-name" +
537            "2: printLists();" +
538            "3: addAddress(argv[1], argv[2]);    ... SPECIFICALLY: argv[0]=\"3\", argv[1]=list-name, argv[2]=e-mail address" +
539            "4: removeAddress(argv[1], argv[2]); ... SPECIFICALLY: argv[0]=\"4\", argv[1]=list-name, argv[2]=e-mail address"
540        );
541
542        System.exit(0);
543    }
544
545    /**
546     * This will facilitate a (very) small console-driven command program to add or remove e-mail
547     * lists to and from already existent lists.  If incorrect command-line arguments are not
548     * provided, the help menu is printed to console.
549     */
550    public static void main(String argv[]) throws IOException
551    {
552        if (argv.length < 1) printMenu();
553        
554        if (argv[0].equals("1"))
555            if (argv.length != 2)   printMenu();
556            else                    FileRW.writeFile(argv[1] + "\n", "EMAIL_LISTS_TEMP_FILE.txt");
557
558        EMailLists lists = EMailLists.loadFromDisk
559            (FileRW.loadFileToString("EMAIL_LISTS_TEMP_FILE.txt").trim());
560
561        if (argv[0].equals("2"))
562            if (argv.length != 1)   printMenu();
563            else                    System.out.print(lists.listsToString());
564
565        if (argv[0].equals("3"))
566            if (argv.length != 3)   printMenu();
567            else                    { lists.addAddress(argv[1], argv[2]); lists.writeToDisk(); }
568
569        if (argv[0].equals("4"))
570            if (argv.length != 3)   printMenu(); 
571            else                    { lists.removeAddress(argv[1], argv[2]); lists.writeToDisk(); }
572    }
573}