/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2003 - 2007 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package com.funambol.syncclient.blackberry.email.impl;
import java.util.Hashtable;
import java.util.Vector;
import java.util.Date;
import java.util.Calendar;
import java.util.TimeZone;
import net.rim.blackberry.api.mail.Header;
import net.rim.blackberry.api.mail.Message;
import net.rim.blackberry.api.mail.Folder;
import net.rim.blackberry.api.mail.Store;
import net.rim.blackberry.api.mail.Session;
import net.rim.device.api.xml.parsers.*;
import net.rim.device.api.io.http.HttpDateParser;
import org.w3c.dom.*;
import org.xml.sax.*;
import com.funambol.syncclient.common.Base64;
import com.funambol.syncclient.util.StaticDataHelper;
import net.rim.blackberry.api.mail.Multipart;
import net.rim.blackberry.api.mail.SupportedAttachmentPart;
import net.rim.blackberry.api.mail.TextBodyPart;
import net.rim.blackberry.api.mail.FolderNotFoundException;
import net.rim.blackberry.api.mail.Address;
/**
* To parse MIME content in SyncML
*
*/
public class MIMEParser
{
/**
*
* This method processes the MIME data and returns the message object
*
*/
public Message parseMessage(String messageString,String locURI) {
Message msg = new Message();
Hashtable ht = new Hashtable();
try {
messageString = extractMIMEContent(messageString);
ht = CreateHashTable(messageString);
MailContent mailContent = new MailContent((String)ht.get("CONTENT-TYPE"));
mailContent.parseContent();
MailBody body = mailContent.getMailBody();
Vector attachment = mailContent.getAttachmentList();
msg.addHeader("FROM", (String)ht.get("FROM"));
msg.addHeader("TO", (String)ht.get("TO"));
msg.setHeader("DATE", (String)ht.get("DATE"));
msg.setSentDate(parseRfc822Date((String)ht.get("DATE")));
// Set sent date, shown when message is opened
msg.addHeader("MIME-Version", (String)ht.get("MIME-VERSION"));
// FIXME: should be set the received date too, if possible.
MessageIDStore msgIdStore = new MessageIDStore();
msgIdStore.setMessageMappingKey();
msgIdStore.add(locURI, (String)ht.get("MESSAGE-ID"));
msg.setSubject((String)ht.get("SUBJECT"));
if ((String)ht.get("FROM") != null) {
String strFrom = (String)ht.get("FROM");
if(strFrom.indexOf("<") >= 0){
strFrom = strFrom.substring(strFrom.indexOf("<") + 1, strFrom.indexOf(">"));
}
Address from = new Address(strFrom.trim(), " ");
msg.setFrom(from);
}
// FIXME: What if there is more than one To: ??
if ((String)ht.get("TO") != null) {
String strTo = (String)ht.get("TO");
if (strTo.indexOf("<") >= 0) {
strTo = strTo.substring(strTo.indexOf("<") + 1, strTo.indexOf(">"));
}
Address to[] = new Address[1];
to[0] = new Address(strTo.trim(), " ");
msg.addRecipients(Message.RecipientType.TO, to);
}
/*
while (strTo.indexOf("<") >= 0) {
addr = strTo.substring(strTo.indexOf("<") + 1, strTo.indexOf(">"));
Arrays.add(to, new Address(addr.trim(), " "));
}
msg.addRecipients(Message.RecipientType.TO, to);
*/
if (attachment.size() == 0) {
if (body.getActualContent() != null) {
String encoding = (String)ht.get("CONTENT-TRANSFER-ENCODING");
if (encoding != null && encoding.trim().equalsIgnoreCase("base64")) {
msg.setContent(new String(Base64.decode(body.getActualContent().getBytes())));
} else {
msg.setContent(body.getActualContent());
}
}
} else {
Multipart mp = new Multipart();
TextBodyPart bodypart = new TextBodyPart(mp);
int bodyPartIndex = 0;
if (body.getActualContent() != null) {
String encoding = body.getContentTransferEncoding();
bodypart.setContentType(body.getContentType());
String content = null;
if (encoding != null && encoding.trim().equalsIgnoreCase("base64")){
content = new String(Base64.decode(body.getActualContent().getBytes()));
} else {
content = body.getActualContent();
}
bodypart.setContent(content);
mp.addBodyPart(bodypart,bodyPartIndex);
++bodyPartIndex;
}
for (int i = 0; i < attachment.size(); i++) {
SupportedAttachmentPart sp = new SupportedAttachmentPart(mp);
MailAttachment mailAttachment = (MailAttachment)attachment.elementAt(i);
sp.setFilename(mailAttachment.getFileName());
if (mailAttachment.getContentTransferEncoding().equals("base64")){
sp.setContent(new String(Base64.decode(mailAttachment.getactualContent().getBytes())));
}
else {
sp.setContent(mailAttachment.getactualContent());
}
sp.setContentType(mailAttachment.getContentType());
mp.addBodyPart(sp,bodyPartIndex);
++bodyPartIndex;
}
msg.setContent(mp);
}
} catch(FolderNotFoundException f) {
StaticDataHelper.log("FolderNotFoundException in MIMEParser.parseMessage(): "
+ f.getMessage());
} catch(Exception e) {
StaticDataHelper.log("Exception in MIMEParser.parseMessage(): " + e.getMessage());
}
return msg;
}
/**
* Helper method to parse the data, this method populates the
* Hashtable with the headers of the message and returns it.
*/
public Hashtable CreateHashTable(String msg) throws Exception {
String boundary = "''~~";//Intialise boundary to some junk value
Hashtable htable = new Hashtable();
// Split the content in lines and iterate through them
String[] splitVals = new StringUtil().split(msg,"\n");
for (int i = 0; i < splitVals.length; i++) {
String temp = splitVals[i];
// This is a boundary in case of multipart messages
if (temp.trim().equals(boundary)) {
StringBuffer section = new StringBuffer();
for (int j = i; j < splitVals.length; j++) {
section.append(splitVals[j]).append("\n");
}
String contentType = (String) htable.get("CONTENT-TYPE");
// The part content is stored in the hashtable along with the
// content-type... No comment.
if (contentType != null) {
htable.put("CONTENT-TYPE", contentType + section.toString());
}
break;
}
// This is an empty line: it is the separation between the header
// and the body of the message.
if ( temp.trim().length()==0 ) {
if (StringUtil.removeWideSpaces(temp).equals("\r")
|| StringUtil.removeWideSpaces(temp).equals("\r\r")) {
if (htable.containsKey("CONTENT-TYPE")) {
String contentType=(String)htable.get("CONTENT-TYPE");
String helper=contentType.toLowerCase();
if ((contentType.trim().length() != 0)
&& (helper.indexOf("multipart/related")== -1)
&& (helper.indexOf("multipart/alternative")== -1)
&& (helper.indexOf("multipart/mixed")== -1)) {
StringBuffer section=new StringBuffer();
for (int j=i;j<splitVals.length;j++) {
section.append(splitVals[j]).append("\n");
}
htable.put("CONTENT-TYPE",contentType+section.toString());
break;
}
}
}
}
// Find the header separator in the line
String[] keyValue = new StringUtil().split(temp, ":");
// This is an header, parse it
if (keyValue.length == 2) {
String key = keyValue[0].toUpperCase().trim();
String value = keyValue[1];
if ((key.equalsIgnoreCase("Content-Disposition"))
// FIXME: what if the filename is not in the next line?
&& (splitVals[i+1].toLowerCase().indexOf("filename") >= 0)) {
htable.put("CONTENT-DISPOSITION", value + "\n");
String[] file = new StringUtil().split(splitVals[i+1], "=");
htable.put("FILENAME", file[1]);
++i;
} else if (key.equalsIgnoreCase("Content-Type")) {
if (value.equals("*/*\r")) {
htable.put("CONTENT-TYPE", value + "\n");
}
else if (!(value.indexOf("multipart/related") < 0)) {
i++;
htable.put("CONTENT-TYPE", value + "\n" + splitVals[i]+ "\n");
boundary = "--" + findBoundary(splitVals[i]) ;
}
else if (!(value.indexOf("multipart/mixed") < 0)) {
i++;
htable.put("CONTENT-TYPE", value + "\n" + splitVals[i]+ "\n");
boundary = "--" + findBoundary(splitVals[i]) ;
}
else if (!(value.indexOf("multipart/alternative") < 0)) {
i++;
htable.put("CONTENT-TYPE", value + "\n" + splitVals[i]+ "\n");
boundary = "--" + findBoundary(splitVals[i]) ;
}
else{
if(splitVals[i+1].indexOf("charset=") >= 0) {
value = value+splitVals[i+1].trim();
++i;
}
htable.put("CONTENT-TYPE", value + "\r\n");
}
}
else {
// Store the other headers
String storedValue = (String) htable.get(key);
if (storedValue != null) {
value = storedValue + "[``-<==>-``]" + value;
}
htable.put(key.trim(), value);
}
}
else if (keyValue.length > 2) {
// Store the headers with a colon inside the value
// FIXME: could this happen for multipart header too?
String value = new String();
String key = keyValue[0].toUpperCase();
for (int tempi = 1; tempi < keyValue.length; tempi++){
value = value + ":" + keyValue[tempi];
}
value = value.substring(1, value.length());
htable.put(key.trim(), value);
}
}
return htable;
}
/**
*
* This method find the boundry in the MIME data
*
*/
public static String findBoundary(String content) {
String copy = content.toLowerCase();
int startIndex = copy.indexOf("boundary=\"");
int endIndex = copy.indexOf("\"",startIndex+10);
if (startIndex >= 0 && endIndex >= 0) {
return content.substring(startIndex+10, endIndex);
}
startIndex = copy.indexOf("boundary=");
endIndex = copy.indexOf(";");
if (endIndex==-1) {
endIndex = copy.indexOf("\r");
}
if (startIndex > 0 && endIndex > 0) {
return content.substring(startIndex + 9, endIndex).trim();
}
return null;
}
/**
*
* This method extracts the required MIME data from the message
*
*/
private String extractMIMEContent(String content) {
String DATA_START_MARKER = "<![CDATA[";
String DATA_END_MARKER = "]]>";
String mimeData = null;
int startMarkerIndex = content.indexOf(DATA_START_MARKER);
int endMarkerIndex = content.indexOf(DATA_END_MARKER);
if (startMarkerIndex == -1 || endMarkerIndex == -1) {
return null;
}
return mimeData = content.substring((startMarkerIndex
+ DATA_START_MARKER.length()),endMarkerIndex);
}
// TODO: create a class derived from Date to handle the rfc822 date
// parsing and formatting. Also the getTimeZoneOffset can be a method
// of this class, now it's duplicated!
/**
* @param date date to read offset
* @return timezone offset
*/
private long getTimeZoneOffSet(Date date) {
Calendar now = null;
TimeZone zn = null;
long offset = 0;
/*
* Gets a calendar using the
* default time zone and locale
*/
now = Calendar.getInstance();
now.setTime(date);
/*
* creates a TimeZone based
* on the time zone where the
* program is running
*/
zn = TimeZone.getDefault();
/*
* the offset to add to GMT to
* get local time, modified in
* case of daylight savings
*/
offset = zn.getOffset(1 ,//era AD
now.get(Calendar.YEAR) ,
now.get(Calendar.MONTH) ,
now.get(Calendar.DAY_OF_MONTH) ,
now.get(Calendar.DAY_OF_WEEK) ,
now.get(Calendar.HOUR_OF_DAY) * 60 * 60 * 1000 +//the milliseconds
now.get(Calendar.MINUTE) * 60 * 1000 +//in day in standard
now.get(Calendar.SECOND) * 1000 );//local time
return offset;
}
/*
* Parse the string in RFC822 format and return a Date object.
* On error, log and return the current date.
*/
private Date parseRfc822Date(String d)
{
Hashtable monthNames = new Hashtable();
monthNames.put("Jan", new Integer(0));
monthNames.put("Feb", new Integer(1));
monthNames.put("Mar", new Integer(2));
monthNames.put("Apr", new Integer(3));
monthNames.put("May", new Integer(4));
monthNames.put("Jun", new Integer(5));
monthNames.put("Jul", new Integer(6));
monthNames.put("Aug", new Integer(7));
monthNames.put("Sep", new Integer(8));
monthNames.put("Oct", new Integer(9));
monthNames.put("Nov", new Integer(10));
monthNames.put("Dec", new Integer(11));
try {
String [] ids = TimeZone.getAvailableIDs();
Calendar cal = Calendar.getInstance();
StaticDataHelper.log("Date original: " + d );
int start = d.indexOf(',');
// Just skip the weekday if present
start = (start == -1) ? 0 : start + 2;
// Get day of month
int end = d.indexOf(' ', start);
cal.set(cal.DAY_OF_MONTH, Integer.parseInt(d.substring(start,end)));
// Get month
start = end + 1;
end = d.indexOf(' ', start);
cal.set(cal.MONTH, ((Integer)monthNames.get(d.substring(start,end))).intValue());
// Get year
start = end + 1;
end = d.indexOf(' ', start);
cal.set(cal.YEAR, Integer.parseInt(d.substring(start,end)));
// Get hour
start = end + 1;
end = d.indexOf(':', start);
cal.set(cal.HOUR_OF_DAY, Integer.parseInt(d.substring(start,end)));
// Get min
start = end + 1;
end = d.indexOf(':', start);
cal.set(cal.MINUTE, Integer.parseInt(d.substring(start,end)));
// Get sec
start = end + 1;
end = d.indexOf(' ', start);
cal.set(cal.SECOND, Integer.parseInt(d.substring(start,end)));
Date ret = cal.getTime();
// Get timezone
start = end + 1;
end = d.indexOf('\r', start);
String tzString = null;
if (end != -1){
tzString = d.substring(start, end);
}
else {
tzString = d.substring(start);
}
// Process timezone (TimeZone class does not work with this format)
if(tzString.charAt(0) == '+' || tzString.charAt(0) == '-'){
long tzoffset = getTimeZoneOffSet(ret);
try {
// Skip the '+' or parseInt fails...
int st = (tzString.charAt(0) == '-') ? 0 : 1;
String hs = tzString.substring(st,3);
String ms = tzString.substring(3,5);
long hh = Integer.parseInt(hs);
long mm = Integer.parseInt(ms);
// Adjust time
StaticDataHelper.log("Date Local: " + ret );
long tt = ret.getTime() + tzoffset;
StaticDataHelper.log("Date adjusted: " + new Date(tt) );
tt -= (hh * 3600 + mm * 60) * 1000;
ret = new Date(tt);
}
catch (NumberFormatException nfe){
StaticDataHelper.log("Error decoding tz.:" + tzString);
}
}
else {
TimeZone tz = TimeZone.getTimeZone(tzString);
cal.setTimeZone(tz);
ret = cal.getTime();
}
StaticDataHelper.log("Date parsed and converted: " + ret );
return ret;
}
catch (Exception e){
StaticDataHelper.log("Exception in parseRfc822Date: " + e.toString());
// Return the current date
return Calendar.getInstance().getTime();
}
}
// TODO (see above) --------------------------------------------------------
}