24 January, 2005

Playing with Thunderbird code

Modify thunderbird so it will append some text at the end of mail body

It seems a easy task, but in fact, I can only achieve via modifying the code (Not just by using preference/cmdline argument). The modification is just a few lines, but it does cost me a lot of time to study. Meanwhile, I've learn quite a lot about Mozilla. In this post, I emphasis on how I modify the code. Other related stuff (build system, localization, etc) should be in anohter post :)

Current situation:

Mozilla already accept a cmdline argument which will do similar stuff $ thunderbird -compose 'to=foo@no.net,attachment=file:///tmp/a.txt' However, it isn't exactly what I want. In mail composer, user can have a preference called "signature", you can define a file which will be automatically appended to the mail body, whenever you compose a new mail. This doesn't solve my problem though, because this options is per user profile. Also, if I use this signature as the "mail footer' as I want, user will be able to alter it easily. On the other hand, user can't use both the "signature" and the "mailfooter" at the same time. Hence, I decide to make thunderbird accept one more parameters: mailfooter=/tmp/text.txt So, if I execute the cmdline like this: $ thunderbird -compose 'to=foo@nowhere.net,mailfooter=/tmp/text.txt' Thunderbird will start the mail composer and append /tmp/text.txt to the mail body, just like how it due with "signature".

So, how to archieve this?

Mozilla is component based. The core technology is called 'xpcom' which stands for 'Cross Platform COM'. Basically most of the functionality as packages as component. The interface of the component are defined in .idl files. On the other hand, all UI related stuff are packaged inside "chrome", eg: translation, UI, etc. The file 'mail/components/compose/MsgComposeCommand.js' is the file responsible for parsing the -compose argument and control the UI interaction (handling UI event.). In mozilla, the UI is defined in XUL, the core logic is component which are actually some shared object (eg libmail.so). "MsgComposeCommand.js" is the glue between the XUL and the components. The modification of "MsgComposeCommand.js":
# Pass 'mailfooter' argument to nsIMsgComposeParams object, (then pass to nsIMsgCompose)
@@ -1206,6 +1206,8 @@
        params.originalMsgURI = args.originalMsg;
        if (args.preselectid)
          params.identity = getIdentityForKey(args.preselectid);
+       if (args.mailfooter)
+         params.mailFooterPath = args.mailfooter;
        if (args.to)
          composeFields.to = args.to;
        if (args.cc)

extremem trivial:
In the function ComposeStartup. "params" is a nsIMsgComposeParams, we just put "args.mailfooter" to this object. The nsIMsgCompose object will use this piece of data. Well, nsIMsgComposeParams is one of the components, which need to be modified, so that we can set/get the attributes "mailFooterPath". In order to achieve this, modify these files:
  • mailnews/compose/public/nsIMsgComposeParams.idl (the interface should have the new attr)
  • mailnews/compose/src/nsMsgComposeParams.h (the component should have the new attr)
  • mailnews/compose/src/nsMsgComposeParams.cpp (need to provide the getter/setter func)
And the following is the diff:
--- nsIMsgComposeParams.idl     2001-09-29 04:06:51.000000000 +0800
+++ nsIMsgComposeParams.idl     2005-01-24 10:57:40.000000000 +0800
@@ -76,6 +76,7 @@
   attribute MSG_ComposeType       type;
   attribute MSG_ComposeFormat     format;
   attribute string                originalMsgURI;
+  attribute string                mailFooterPath;
   attribute nsIMsgIdentity        identity;
                                                                               
   attribute nsIMsgCompFields      composeFields;

--- nsMsgComposeParams.h      2001-09-29 04:06:54.000000000 +0800
+++ nsMsgComposeParams.h   2005-01-24 10:57:40.000000000 +0800
@@ -51,6 +51,7 @@
   MSG_ComposeType               mType;
   MSG_ComposeFormat             mFormat;
   nsCString                     mOriginalMsgUri;
+  nsCString                     mMailFooterPath;
   nsCOMPtr      mIdentity;
   nsCOMPtr    mComposeFields;
   PRBool                        mBodyIsLink;

--- nsMsgComposeParams.cpp    2003-09-08 06:55:30.000000000 +0800
+++ nsMsgComposeParams.cpp 2005-01-24 10:57:40.000000000 +0800
@@ -95,6 +95,20 @@
     return NS_OK;
   }

+  /* attribute string mailFooterMail; */
+  NS_IMETHODIMP nsMsgComposeParams::GetMailFooterPath(char * *aMailFooterPath)
+  {
+    NS_ENSURE_ARG_POINTER(aMailFooterPath);
+
+    *aMailFooterPath = ToNewCString(mMailFooterPath);
+    return NS_OK;
+  }
+  NS_IMETHODIMP nsMsgComposeParams::SetMailFooterPath(const char * aMailFooterPath)
+  {
+    mMailFooterPath = aMailFooterPath;
+    return NS_OK;
+  }
+
   /* attribute nsIMsgIdentity identity; */
   NS_IMETHODIMP nsMsgComposeParams::GetIdentity(nsIMsgIdentity * *aIdentity)
   {
Finally, I need to modify nsMsgCompose.cpp and nsMsgCompose.h. This component will responsible to represent a mail composition. During nsMsgCompose::Initialize() will shall extract relevant info from the input parameter nsMsgComposeParams, so it should extract the 'mailfooter' parameter and store it inside the object. Then, when nsMsgCompose::InitEditor() is called. The call trace will be something like:
> InitEditor()
   > BuildBodyMessageAndSignature()
      > ProcessSignature()
      > ConvertAndLoadComposeWindow()
The modification will be: after calling ProcessSignature(), I made another call "ProcessFooter()", will function need to be implement (but it should be similar to ProcessSignature), after that, append the output of ProcessFooter into tSignature (string object) which will further handled by ConvertAndLoadComposeWindow(). The following is the final piece of modification:
--- nsIMsgCompose.idl   2004-05-17 14:14:57.000000000 +0800
+++ nsIMsgCompose.idl   2005-01-24 10:57:40.000000000 +0800
@@ -234,6 +234,10 @@
                                   in boolean aQuoted,
                                   inout nsString aMsgBody);

+  /* Append the footer defined in the cmdline argument */
+  [noscript] void processFooter(in nsStringRef aPath,
+                                inout nsString aMsgBody);
+
   /* set any reply flags on the original message's folder */
   [noscript] void processReplyFlags();
   [noscript] void rememberQueuedDisposition();

--- nsMsgCompose.cpp       2004-10-29 14:51:35.000000000 +0800
+++ nsMsgCompose.cpp       2005-01-24 11:00:23.000000000 +0800
@@ -706,6 +706,10 @@
   nsXPIDLCString originalMsgURI;
   params->GetOriginalMsgURI(getter_Copies(originalMsgURI));
                                                                                
+  nsXPIDLCString mailFooterPath;
+  params->GetMailFooterPath(getter_Copies(mailFooterPath));
+  mMailFooterPath = mailFooterPath;
+
   nsCOMPtr composeFields;
   params->GetComposeFields(getter_AddRefs(composeFields));
                                                                                
@@ -3322,6 +3326,134 @@
 }
                                                                                
 //
+// This will process the mail footer file for the user. This method
+// will always append the results to the mMsgBody member variable.
+//
+nsresult
+nsMsgCompose::ProcessFooter(nsString& aPath, nsString *aMsgBody)
+{
+  nsresult      rv = NS_OK;
+
+  // Note: We will have intelligent signature behavior in that we
+  // look at the signature file first...if the extension is .htm or
+  // .html, we assume its HTML, otherwise, we assume it is plain text
+  PRBool        useSigFile = PR_FALSE;
+  PRBool        htmlSig = PR_FALSE;
+  nsAutoString  sigData;
+  nsAutoString  sigOutput;
+
+  useSigFile = PR_FALSE;
+  if (!aPath.IsEmpty())
+  {
+    // XXX create nsILocalFile from 'path'
+    nsCOMPtr sigFile;
+    rv = NS_NewLocalFile (aPath, PR_TRUE, getter_AddRefs(sigFile));
+    //rv = sigFile->InitWithPath(aPath);
+    if (NS_SUCCEEDED(rv)) {
+      useSigFile = PR_TRUE; // ok, there's a signature file
+      // Now, most importantly, we need to figure out what the content type is for
+      // this signature...if we can't, we assume text
+      nsXPIDLCString sigContentType;
+      nsresult rv2; // don't want to clobber the other rv
+      nsCOMPtr mimeFinder (do_GetService(NS_MIMESERVICE_CONTRACTID, &rv2));
+      if (NS_SUCCEEDED(rv2)) {
+        rv2 = mimeFinder->GetTypeFromFile(sigFile, getter_Copies(sigContentType));
+        if (NS_SUCCEEDED(rv2)) {
+          if (sigContentType.Equals(TEXT_HTML, nsCaseInsensitiveCStringComparator()))
+            htmlSig = PR_TRUE;
+        }
+      }
+    }
+  }
+
+  // Now, if they didn't even want to use a signature, we should
+  // just return nicely.
+  if (!useSigFile || NS_FAILED(rv))
+    return NS_OK;
+
+  nsFileSpec    testSpec(aPath);
+  //nsFileSpec    testSpec("/tmp/tmp.txt", 12);
+
+  // If this file doesn't really exist, just bail!
+  if (!testSpec.Exists())
+    return NS_OK;
+
+  static const char      htmlBreak[] = "
"; + static const char dashes[] = "-- "; + static const char htmlsigopen[] = "
"; + static const char htmlsigclose[] = "
"; /* XXX: Due to a bug in + 4.x' HTML editor, it will not be able to + break this HTML sig, if quoted (for the user to + interleave a comment). */ + static const char _preopen[] = "
";
+  char*                  preopen;
+  static const char      preclose[] = "
"; + + PRInt32 wrapLength = 72; // setup default value in case GetWrapLength failed + GetWrapLength(&wrapLength); + preopen = PR_smprintf(_preopen, wrapLength); + if (!preopen) + return NS_ERROR_OUT_OF_MEMORY; + + // is this a text sig with an HTML editor? + if ( (m_composeHTML) && (!htmlSig) ) + ConvertTextToHTML(testSpec, sigData); + // is this a HTML sig with a text window? + else if ( (!m_composeHTML) && (htmlSig) ) + ConvertHTMLToText(testSpec, sigData); + else // We have a match... + LoadDataFromFile(testSpec, sigData); // Get the data! + + // Now that sigData holds data...if any, append it to the body in a nice + // looking manner + if (!sigData.IsEmpty()) + { + if (m_composeHTML) + { + sigOutput.AppendWithConversion(htmlBreak); + if (htmlSig) + sigOutput.AppendWithConversion(htmlsigopen); + else + sigOutput.AppendWithConversion(preopen); + } + else + sigOutput.AppendWithConversion(CRLF); + + if (sigData.Find("\r-- \r", PR_TRUE) < 0 && + sigData.Find("\n-- \n", PR_TRUE) < 0 && + sigData.Find("\n-- \r", PR_TRUE) < 0) + { + nsDependentSubstring firstFourChars(sigData, 0, 4); + + if (!(firstFourChars.Equals(NS_LITERAL_STRING("-- \n")) || + firstFourChars.Equals(NS_LITERAL_STRING("-- \r")))) + { + sigOutput.AppendWithConversion(dashes); + + if (!m_composeHTML || !htmlSig) + sigOutput.AppendWithConversion(CRLF); + else if (m_composeHTML) + sigOutput.AppendWithConversion(htmlBreak); + } + } + + sigOutput.Append(sigData); + + if (m_composeHTML) + { + if (htmlSig) + sigOutput.AppendWithConversion(htmlsigclose); + else + sigOutput.AppendWithConversion(preclose); + } + } + + aMsgBody->Append(sigOutput); + PR_Free(preopen); + return NS_OK; +} + +// // This will process the signature file for the user. This method // will always append the results to the mMsgBody member variable. // @@ -3555,6 +3687,17 @@ if (addSignature) ProcessSignature(m_identity, PR_FALSE, &tSignature); + // Footer is 'special feature' added for ThizMII + PRBool addFooter = PR_TRUE; + nsAutoString tFooter; + + if (!mMailFooterPath.IsEmpty()) { + nsAutoString tFooterPath = NS_ConvertASCIItoUTF16(mMailFooterPath); + ProcessFooter(tFooterPath, &tFooter); + } + + tSignature.Append(tFooter); + // if type is new, but we have body, this is probably a mapi send, so we need to // replace '\n' with
so that the line breaks won't be lost by html. // if mailtourl, do the same.
To conclude, I have edited the following:
  • MsgComposeCommand.js
  • nsIMsgComposeParams.idl
  • nsMsgComposeParams.cpp
  • nsMsgComposeParams.h
  • nsIMsgCompose.idl
  • nsMsgCompose.cpp
  • nsMsgCompose.h
And the basic logic is to let MsgComposeCommand.js parse the 'mailfooter=XXX' parameters, and inside MsgCompose object, actually read the file and append to the mail body.