1   /*
2    *  EmailDocumentHandler.java
3    *
4    *  Copyright (c) 1998-2005, The University of Sheffield.
5    *
6    *  This file is part of GATE (see http://gate.ac.uk/), and is free
7    *  software, licenced under the GNU Library General Public License,
8    *  Version 2, June 1991 (in the distribution as file licence.html,
9    *  and also available at http://gate.ac.uk/gate/licence.html).
10   *
11   *  Cristian URSU,  3/Aug/2000
12   *
13   *  $Id: EmailDocumentHandler.java,v 1.27 2005/01/11 13:51:34 ian Exp $
14   */
15  
16  package gate.email;
17  
18  import java.io.*;
19  import java.util.*;
20  
21  import junit.framework.Assert;
22  
23  import gate.*;
24  import gate.event.StatusListener;
25  
26  /**
27    * This class implements the behaviour of the Email reader
28    * It takes the Gate Document representing a list with e-mails and
29    * creates Gate annotations on it.
30    */
31  public class EmailDocumentHandler{
32  
33    /** Debug flag */
34    private static final boolean DEBUG = false;
35  
36    private String content = null;
37    private long documentSize = 0;
38  
39    /**
40      * Constructor used in tests mostly
41      */
42    public EmailDocumentHandler() {
43      setUp();
44    }//EmailDocumentHandler
45  
46    /**
47      * Constructor initialises some private fields
48      */
49    public EmailDocumentHandler( gate.Document aGateDocument,
50                                 Map  aMarkupElementsMap,
51                                 Map  anElement2StringMap
52                                ) {
53  
54      gateDocument = aGateDocument;
55  
56      // gets AnnotationSet based on the new gate document
57      if (basicAS == null)
58        basicAS = gateDocument.getAnnotations(
59                                  GateConstants.ORIGINAL_MARKUPS_ANNOT_SET_NAME);
60  
61      markupElementsMap = aMarkupElementsMap;
62      element2StringMap = anElement2StringMap;
63      setUp();
64    }// EmailDocumentHandler
65  
66    /**
67      * Reads the Gate Document line by line and does the folowing things:
68      * <ul>
69      * <li> Each line is analized in order to detect where an e-mail starts.
70      * <li> If the line belongs to an e-mail header then creates the
71      *      annotation if the markupElementsMap allows that.
72      * <li> Lines belonging to the e-mail body are placed under a Gate
73      *      annotation called messageBody.
74      * </ul>
75      */
76    public void annotateMessages() throws IOException,
77                                          gate.util.InvalidOffsetException {
78      // obtain a BufferedReader form the Gate document...
79      BufferedReader gateDocumentReader = null;
80      // Get the string representing the content of the document
81      // It is used inside CreateAnnotation method
82      content = gateDocument.getContent().toString();
83      // Get the sieze of the Gate Document. For the same purpose.
84      documentSize = gateDocument.getContent().size().longValue();
85  
86  //    gateDocumentReader = new BufferedReader(new InputStreamReader(
87  //              gateDocument.getSourceUrl().openConnection().getInputStream()));
88      gateDocumentReader = new BufferedReader(new InputStreamReader(
89                                  new ByteArrayInputStream(content.getBytes())));
90  
91      // for each line read from the gateDocumentReader do
92      // if the line begins an e-mail message then fire a status listener, mark
93      // that we are processing an e-mail, update the cursor and go to the next
94      // line.
95  
96      // if we are inside an e-mail, test if the line belongs to the message
97      // header
98      // if so, create a header field annotation.
99  
100     // if we are inside a a body and this is the first line from the body,
101     // create the message body annotation.
102     // Otherwise just update the cursor and go to the next line
103 
104     // if the line doesn't belong to an e-mail message then just update the
105     // cursor.
106     // next line
107 
108     String line = null;
109     String aFieldName = null;
110 
111     long cursor = 0;
112     long endEmail = 0;
113     long startEmail = 0;
114     long endHeader = 0;
115     long startHeader = 0;
116     long endBody = 0;
117     long startBody = 0;
118     long endField = 0;
119     long startField = 0;
120 
121     boolean insideAnEmail   = false;
122     boolean insideHeader    = false;
123     boolean insideBody      = false;
124     boolean emailReadBefore = false;
125     boolean fieldReadBefore = false;
126 
127     long nlSize = detectNLSize();
128 
129     //Out.println("NL SIZE = " + nlSize);
130 
131     // read each line from the reader
132     while ((line = gateDocumentReader.readLine()) != null){
133       // Here we test if the line delimitates two e-mail messages.
134       // Each e-mail message begins with a line like this:
135       // From P.Fairhurst Thu Apr 18 12:22:23 1996
136       // Method lineBeginsMessage() detects such lines.
137       if (lineBeginsMessage(line)){
138             // Inform the status listener to fire only
139             // if no. of elements processed.
140             // So far is a multiple of ELEMENTS_RATE
141           if ((++ emails % EMAILS_RATE) == 0)
142             fireStatusChangedEvent("Reading emails : " + emails);
143           // if there are e-mails read before, then the previous e-mail
144           // ends here.
145           if (true == emailReadBefore){
146             // Cursor points at the beggining of the line
147             // E-mail and Body ends before the \n char
148             // Email ends as cursor value indicates
149             endEmail = cursor - nlSize ;
150             // also the e-mail body ends when an e-mail ends
151             endBody = cursor - nlSize;
152             //Annotate an E-mail body (startBody, endEmail)
153             createAnnotation("Body",startBody,endBody,null);
154             //Annotate an E-mail message(startEmail, endEmail) Email starts
155             createAnnotation("Message",startEmail,endEmail,null);
156           }
157           // if no e-mail was read before, now there is at list one message
158           // read
159           emailReadBefore = true;
160           // E-mail starts imediately from the beginning of this line which
161           // sepatates 2 messages.
162           startEmail = cursor;
163           // E-mail header starts also from here
164           startHeader = cursor;
165           // The cursor is updated with the length of the line + the
166           // new line char
167           cursor += line.length() + nlSize;
168           // We are inside an e-mail
169           insideAnEmail = true;
170           // Next is the E-mail header
171           insideHeader = true;
172           // No field inside header has been read before
173           fieldReadBefore = false;
174           // Read the next line
175           continue;
176       }//if (lineBeginsMessage(line))
177       if (false == insideAnEmail){
178         // the cursor is update with the length of the line +
179         // the new line char
180         cursor += line.length() + nlSize;
181         // read the next line
182         continue;
183       }//if
184       // here we are inside an e-mail message (inside Header or Body)
185       if (true == insideHeader){
186         // E-mail spec sais that E-mail header is separated by E-mail body
187         // by a \n char
188         if (line.equals("")){
189           // this \n sepatates the header of an e-mail form its body
190           // If we are here it means that the header has ended.
191           insideHeader  = false;
192           // e-mail header ends here
193           endHeader = cursor - nlSize;
194           // update the cursor with the length of \n
195           cursor += line.length() + nlSize;
196           // E-mail body starts from here
197           startBody = cursor;
198           // if fields were read before, it means that the e-mail has a header
199           if (true == fieldReadBefore){
200             endField = endHeader;
201             //Create a field annotation (fieldName, startField, endField)
202             createAnnotation(aFieldName, startField, endField, null);
203             //Create an e-mail header annotation
204             createAnnotation("Header",startHeader,endHeader,null);
205           }//if
206           // read the next line
207           continue;
208         }//if (line.equals(""))
209         // if line begins with a field then prepare to create an
210         // annotation with the name of the field
211         if (lineBeginsWithField(line)){
212           // if a field was read before, it means that the previous field ends
213           // here
214           if (true == fieldReadBefore){
215             // the previous field end here
216             endField = cursor - nlSize;
217             //Create a field annotation (fieldName, startField, endField)
218             createAnnotation(aFieldName, startField, endField, null);
219           }//if
220           fieldReadBefore = true;
221           aFieldName = getFieldName();
222           startField = cursor + aFieldName.length() + ":".length();
223         }//if
224         // in both cases the cursor is updated and read the next line
225         // the cursor is update with the length of the line +
226         // the new line char
227         cursor += line.length() + nlSize;
228         // read the next line
229         continue;
230       }//if (true == insideHeader)
231       // here we are inside the E-mail body
232       // the body will end when the e-mail will end.
233       // here we just update the cursor
234       cursor += line.length() + nlSize;
235     }//while
236     // it might be possible that the file to contain only one e-mail and
237     // if the file contains only one e-mail message then the variable
238     // emailReadBefore must be set on true value
239     if (true == emailReadBefore){
240       endBody  = cursor - nlSize;
241       endEmail = cursor - nlSize;
242       //Annotate an E-mail body (startBody, endEmail)
243       createAnnotation("Body",startBody,endBody,null);
244       //Annotate an E-mail message(startEmail, endEmail) Email starts
245       createAnnotation("Message",startEmail,endEmail,null);
246     }
247     // if emailReadBefore is not set on true, that means that we didn't
248     // encounter any line like this:
249     // From P.Fairhurst Thu Apr 18 12:22:23 1996
250   }//annotateMessages
251 
252   /**
253     * This method detects if the text file which contains e-mail messages
254     * is under MSDOS or UNIX format.
255     * Under MSDOS the size of NL is 2 (\n \r) and under UNIX (\n) the size is 1
256     * @return the size of the NL (1,2 or 0 = if no \n is found)
257     */
258   private int detectNLSize() {
259 
260     // get a char array
261     char[] document = null;
262 
263     // transform the gate Document into a char array
264     document = gateDocument.getContent().toString().toCharArray();
265 
266     // search for the \n char
267     // when it is found test if is followed by the \r char
268     for (int i=0; i<document.length; i++){
269       if (document[i] == '\n'){
270 
271         // we just found a \n char.
272         // here we test if is followed by a \r char or preceded by a \r char
273         if (
274             (((i+1) < document.length) && (document[i+1] == '\r'))
275             ||
276             (((i-1) >= 0)              && (document[i-1] == '\r'))
277            ) return 2;
278         else return 1;
279       }
280     }
281     //if no \n char is found then the document is contained into a single text
282     // line.
283     return 0;
284 
285   } // detectNLSize
286 
287   /**
288     * This method creates a gate annotation given its name, start, end and
289     * feature map.
290     */
291   private void createAnnotation(String anAnnotationName, long anAnnotationStart,
292                                  long anAnnotationEnd, FeatureMap aFeatureMap)
293                                        throws gate.util.InvalidOffsetException{
294 
295 /*
296     while (Character.isWhitespace(content.charAt((int) anAnnotationStart)))
297       anAnnotationStart ++;
298 
299 //    System.out.println(content.charAt((int) anAnnotationEnd));
300     while (Character.isWhitespace(content.charAt((int) anAnnotationEnd)))
301       anAnnotationEnd --;
302 
303     anAnnotationEnd ++;
304 */
305    if (canCreateAnnotation(anAnnotationStart,anAnnotationEnd,documentSize)){
306       if (aFeatureMap == null)
307           aFeatureMap = Factory.newFeatureMap();
308       basicAS.add( new Long(anAnnotationStart),
309                    new Long(anAnnotationEnd),
310                    anAnnotationName.toLowerCase(),
311                    aFeatureMap);
312    }// End if
313   }//createAnnotation
314   /**
315     * This method verifies if an Annotation can be created.
316     */
317   private boolean canCreateAnnotation(long start,
318                                       long end,
319                                       long gateDocumentSize){
320 
321     if (start < 0 || end < 0 ) return false;
322     if (start > end ) return false;
323     if ((start > gateDocumentSize) || (end > gateDocumentSize)) return false;
324     return true;
325   }// canCreateAnnotation
326 
327   /**
328     * Tests if the line begins an e-mail message
329     * @param aTextLine a line from the file containing the e-mail messages
330     * @return true if the line begins an e-mail message
331     * @return false if is doesn't
332     */
333   private boolean lineBeginsMessage(String aTextLine){
334     int score = 0;
335 
336     // if first token is "From" and the rest contains Day, Zone, etc
337     // then this line begins a message
338     // create a new String Tokenizer with " " as separator
339     StringTokenizer tokenizer = new StringTokenizer(aTextLine," ");
340 
341     // get the first token
342     String firstToken = null;
343     if (tokenizer.hasMoreTokens())
344         firstToken = tokenizer.nextToken();
345     else return false;
346 
347     // trim it
348     firstToken.trim();
349 
350     // check against "From" word
351     // if the first token is not From then the entire line can not begin
352     // a message.
353     if (!firstToken.equals("From"))
354         return false;
355 
356     // else continue the analize
357     while (tokenizer.hasMoreTokens()){
358 
359       // get the next token
360       String token = tokenizer.nextToken();
361       token.trim();
362 
363       // see if it has a meaning(analize if is a Day, Month,Zone, Time, Year )
364       if (hasAMeaning(token))
365           score += 1;
366     }
367 
368     // a score greather or equql with 5 means that this line begins a message
369     if (score >= 5) return true;
370     else return false;
371 
372   } // lineBeginsMessage
373 
374   /**
375     * Tests if the line begins with a field from the e-mail header
376     * If the answer is true then it also sets the member fieldName with the
377     * value of this e-mail header field.
378     * @param aTextLine a line from the file containing the e-mail text
379     * @return true if the line begins with a field from the e-mail header
380     * @return false if is doesn't
381     */
382   private boolean lineBeginsWithField(String aTextLine){
383     if (containsSemicolon(aTextLine)){
384       StringTokenizer tokenizer = new StringTokenizer(aTextLine,":");
385 
386       // get the first token
387       String firstToken = null;
388 
389       if (tokenizer.hasMoreTokens())
390         firstToken = tokenizer.nextToken();
391       else return false;
392 
393       if (firstToken != null){
394         // trim it
395         firstToken.trim();
396         if (containsWhiteSpaces(firstToken)) return false;
397 
398         // set the member field
399         fieldName = firstToken;
400       }
401       return true;
402     } else return false;
403 
404   } // lineBeginsWithField
405 
406   /**
407     * This method checks if a String contains white spaces.
408     */
409   private boolean containsWhiteSpaces(String aString) {
410     for (int i = 0; i<aString.length(); i++)
411       if (Character.isWhitespace(aString.charAt(i))) return true;
412     return false;
413   } // containsWhiteSpaces
414 
415   /**
416     * This method checks if a String contains a semicolon char
417     */
418   private boolean containsSemicolon(String aString) {
419     for (int i = 0; i<aString.length(); i++)
420       if (aString.charAt(i) == ':') return true;
421     return false;
422   } // containsSemicolon
423 
424   /**
425     * This method tests a token if is Day, Month, Zone, Time, Year
426     */
427   private boolean hasAMeaning(String aToken) {
428     // if token is a Day return true
429     if (day.contains(aToken)) return true;
430 
431     // if token is a Month return true
432     if (month.contains(aToken)) return true;
433 
434     // if token is a Zone then return true
435     if (zone.contains(aToken)) return true;
436 
437     // test if is a day number or a year
438     Integer dayNumberOrYear = null;
439     try{
440       dayNumberOrYear = new Integer(aToken);
441     } catch (NumberFormatException e){
442       dayNumberOrYear = null;
443     }
444 
445     // if the creation succeded, then test if is day or year
446     if (dayNumberOrYear != null) {
447       int number = dayNumberOrYear.intValue();
448 
449       // if is a number between 1 and 31 then is a day
450       if ((number > 0) && (number < 32)) return true;
451 
452       // if is a number between 1900 si 3000 then is a year ;))
453       if ((number > 1900) && (number < 3000)) return true;
454 
455       // it might be the last two digits of 19xx
456       if ((number >= 0) && (number <= 99)) return true;
457     }
458     // test if is time: hh:mm:ss
459     if (isTime(aToken)) return true;
460 
461    return false;
462   } // hasAMeaning
463 
464   /**
465     * Tests a token if is in time format HH:MM:SS
466     */
467   private boolean isTime(String aToken) {
468     StringTokenizer st = new StringTokenizer(aToken,":");
469 
470     // test each token if is hour, minute or second
471     String hourString = null;
472     if (st.hasMoreTokens())
473         hourString = st.nextToken();
474 
475     // if there are no more tokens, it means that is not a time
476     if (hourString == null) return false;
477 
478     // test if is a number between 0 and 23
479     Integer hourInteger = null;
480     try{
481       hourInteger = new Integer(hourString);
482     } catch (NumberFormatException e){
483       hourInteger = null;
484     }
485     if (hourInteger == null) return false;
486 
487     // if is not null then it means is a number
488     // test if is in 0 - 23 range
489     // if is not in this range then is not an hour
490     int hour = hourInteger.intValue();
491     if ( (hour < 0) || (hour > 23) ) return false;
492 
493     // we have the hour
494     // now repeat the test for minute and seconds
495 
496     // minutes
497     String minutesString = null;
498     if (st.hasMoreTokens())
499         minutesString = st.nextToken();
500 
501     // if there are no more tokens (minutesString == null) then return false
502     if (minutesString == null) return false;
503 
504     // test if is a number between 0 and 59
505     Integer minutesInteger = null;
506     try {
507       minutesInteger = new Integer (minutesString);
508     } catch (NumberFormatException e){
509       minutesInteger = null;
510     }
511 
512     if (minutesInteger == null) return false;
513 
514     // if is not null then it means is a number
515     // test if is in 0 - 59 range
516     // if is not in this range then is not a minute
517     int minutes = minutesInteger.intValue();
518     if ( (minutes < 0) || (minutes > 59) ) return false;
519 
520     // seconds
521     String secondsString = null;
522     if (st.hasMoreTokens())
523         secondsString = st.nextToken();
524 
525     // if there are no more tokens (secondsString == null) then return false
526     if (secondsString == null) return false;
527 
528     // test if is a number between 0 and 59
529     Integer secondsInteger = null;
530     try {
531       secondsInteger = new Integer (secondsString);
532     } catch (NumberFormatException e){
533       secondsInteger = null;
534     }
535     if (secondsInteger == null) return false;
536 
537     // if is not null then it means is a number
538     // test if is in 0 - 59 range
539     // if is not in this range then is not a minute
540     int seconds = secondsInteger.intValue();
541     if ( (seconds < 0) || (seconds > 59) ) return false;
542 
543     // if there are more tokens in st it means that we don't have this format:
544     // HH:MM:SS
545     if (st.hasMoreTokens()) return false;
546 
547     // if we are here it means we have a time
548     return true;
549   }// isTime
550 
551   /**
552     * Initialises the collections with data used by method lineBeginsMessage()
553     */
554   private void setUp(){
555     day = new HashSet();
556     day.add("Mon");
557     day.add("Tue");
558     day.add("Wed");
559     day.add("Thu");
560     day.add("Fri");
561     day.add("Sat");
562     day.add("Sun");
563 
564     month = new HashSet();
565     month.add("Jan");
566     month.add("Feb");
567     month.add("Mar");
568     month.add("Apr");
569     month.add("May");
570     month.add("Jun");
571     month.add("Jul");
572     month.add("Aug");
573     month.add("Sep");
574     month.add("Oct");
575     month.add("Nov");
576     month.add("Dec");
577 
578     zone = new HashSet();
579     zone.add("UT");
580     zone.add("GMT");
581     zone.add("EST");
582     zone.add("EDT");
583     zone.add("CST");
584     zone.add("CDT");
585     zone.add("MST");
586     zone.add("MDT");
587     zone.add("PST");
588     zone.add("PDT");
589   }//setUp
590 
591   /**
592     * This method returns the value of the member fieldName.
593     * fieldName is set by the method lineBeginsWithField(String line).
594     * Each time the the line begins with a field name, that fiels will be stored
595     * in this member.
596     */
597   private String getFieldName() {
598     if (fieldName == null) return new String("");
599     else return fieldName;
600   } // getFieldName
601 
602   // StatusReporter Implementation
603 
604   /**
605     * This methos is called when a listener is registered with this class
606     */
607   public void addStatusListener(StatusListener listener){
608     myStatusListeners.add(listener);
609   }
610   /**
611     * This methos is called when a listener is removed
612     */
613   public void removeStatusListener(StatusListener listener){
614     myStatusListeners.remove(listener);
615   }
616 
617   /**
618     * This methos is called whenever we need to inform the listener
619     * about an event.
620     */
621   protected void fireStatusChangedEvent(String text){
622     Iterator listenersIter = myStatusListeners.iterator();
623     while(listenersIter.hasNext())
624       ((StatusListener)listenersIter.next()).statusChanged(text);
625   }
626 
627   private static final int EMAILS_RATE = 16;
628 
629   // the content of the e-mail document, without any tag
630   // for internal use
631   private String tmpDocContent = null;
632 
633   // a gate document
634   private gate.Document gateDocument = null;
635 
636   // an annotation set used for creating annotation reffering the doc
637   private gate.AnnotationSet basicAS = null;
638 
639   // this map marks the elements that we don't want to create annotations
640   private Map  markupElementsMap = null;
641 
642   // this map marks the elements after we want to insert some strings
643   private Map element2StringMap = null;
644 
645   // listeners for status report
646   protected List myStatusListeners = new LinkedList();
647 
648   // this reports the the number of emails that have beed processed so far
649   private int emails = 0;
650 
651   // this is set by the method lineBeginsWithField(String line)
652   // each time the the line begins with a field name, that fiels will be stored
653   // in this member.
654   private String fieldName = null;
655 
656   private Collection day = null;
657   private Collection month = null;
658   private Collection zone = null;
659 
660 
661  // TEST SECTION
662 
663   /**
664     * Test containsSemicolon
665     */
666   private void testContainsSemicolon() {
667     String str1 = "X-Sender: oana@derwent";
668     String str2 = "X-Sender oana@derwent";
669     String str3 = ":X-Sender oana@derwent";
670     String str4 = "X-Sender oana@derwent:";
671 
672     Assert.assertTrue((containsSemicolon(str1) == true));
673     Assert.assertTrue((containsSemicolon(str2)== false));
674     Assert.assertTrue((containsSemicolon(str3) == true));
675     Assert.assertTrue((containsSemicolon(str4) == true));
676   }// testContainsSemicolon
677 
678   /**
679     * Test containsWhiteSpaces
680     */
681   private void testContainsWhiteSpaces(){
682     String str1 = "Content-Type: TEXT/PLAIN; charset=US-ASCII";
683     String str2 = "Content-Type:TEXT/PLAIN;charset=US-ASCII";
684     String str3 = " Content-Type:TEXT/PLAIN;charset=US-ASCII";
685     String str4 = "Content-Type:TEXT/PLAIN;charset=US-ASCII ";
686 
687     Assert.assertTrue((containsWhiteSpaces(str1) == true));
688     Assert.assertTrue((containsWhiteSpaces(str2) == false));
689     Assert.assertTrue((containsWhiteSpaces(str3) == true));
690     Assert.assertTrue((containsWhiteSpaces(str4) == true));
691   }// testContainsWhiteSpaces
692 
693   /**
694     * Test hasAMeaning
695     */
696   private void testHasAMeaning() {
697     String str1 = "12:05:22";
698     String str2 = "Sep";
699     String str3 = "Fri";
700     String str4 = "2000";
701     String str5 = "GMT";
702     String str6 = "Date: Wed, 13 Sep 2000 13:05:22 +0100 (BST)";
703     String str7 = "12:75:22";
704     String str8 = "September";
705     String str9 = "Friday";
706 
707     Assert.assertTrue((hasAMeaning(str1) == true));
708     Assert.assertTrue((hasAMeaning(str2) == true));
709     Assert.assertTrue((hasAMeaning(str3) == true));
710     Assert.assertTrue((hasAMeaning(str4) == true));
711     Assert.assertTrue((hasAMeaning(str5) == true));
712     Assert.assertTrue((hasAMeaning(str6) == false));
713     Assert.assertTrue((hasAMeaning(str7) == false));
714     Assert.assertTrue((hasAMeaning(str8) == false));
715     Assert.assertTrue((hasAMeaning(str9) == false));
716   } // testHasAMeaning
717 
718   /**
719     * Test isTime
720     */
721   private void testIsTime() {
722     String str1 = "13:05:22";
723     String str2 = "13/05/22";
724     String str3 = "24:05:22";
725 
726     Assert.assertTrue((isTime(str1) == true));
727     Assert.assertTrue((isTime(str2) == false));
728     Assert.assertTrue((isTime(str3) == false));
729   }// testIsTime
730 
731   /**
732     * Test lineBeginsMessage
733     */
734   private void testLineBeginsMessage(){
735     String str1 = "From oana@dcs.shef.ac.uk Wed Sep 13 13:05:23 2000";
736     String str2 = "Date: Wed, 13 Sep 2000 13:05:22 +0100 (BST)";
737     String str3 = "From oana@dcs.shef.ac.uk Sep 13 13:05:23 2000";
738 
739     Assert.assertTrue((lineBeginsMessage(str1) == true));
740     Assert.assertTrue((lineBeginsMessage(str2) == false));
741     Assert.assertTrue((lineBeginsMessage(str3) == false));
742 
743   }// testLineBeginsMessage
744 
745   /**
746     * Test lineBeginsWithField
747     */
748   private void testLineBeginsWithField() {
749     String str1 = "Message-ID: <Pine.SOL.3.91.1000913130311.19537A-10@derwent>";
750     String str2 = "%:ContentType TEXT/PLAIN; charset=US-ASCII";
751 
752     Assert.assertTrue((lineBeginsWithField(str1) == true));
753     Assert.assertTrue((lineBeginsWithField(str2) == true));
754   }// testLineBeginsWithField
755 
756    /**
757      * Test final
758      */
759    public void testSelf(){
760      testContainsSemicolon();
761      testContainsWhiteSpaces();
762      testHasAMeaning();
763      testIsTime();
764      testLineBeginsMessage();
765      testLineBeginsWithField();
766    } // testSelf
767 
768 } //EmailDocumentHandler
769