Bob Balaban's Blog

     
    alt

    Bob Balaban

     

    Calling Notes CAPI from C-sharp/Visual Studio

    Bob Balaban  May 5 2008 12:11:47 PM
    Greetings, Geeks!

    How many of you have ever written LotusScript code that needed to call C entry points in some DLL (such as, maybe, the Notes C API, in nnotes.dll)? I've done it, many of you probably have too.

    Well, I spent a few days recently trying to figure out how to do the same thing, but from a C# ("c-sharp") program running as "managed code" in .NET. I'm using Visual Studio 2005 as my IDE. In the end I cracked it, and it wasn't too difficult. The real secret was to find the magic incantation equivalent to "DECLARE" in LotusScript.

    Background:

    I inherited some C# code that uses the Notes COM interfaces to do some stuff with user mail files. The COM interfaces are pretty much the same as the LotusScript back-end classes, they work great in a .NET environment for accessing NSF data. As an enhancement to the existing code (which loops over all documents in a user mail NSF and does things to them), I wanted to find out which documents were unread for the mail file's owner. This involved two extra steps that I needed to research:
    1.        Figure out the name of the mailbox owner. NotesPeek came to my rescue (as it has so many times before), and I found what I needed. It's a Profile document called "calendarprofile" (just use NotesDatabase.GetProfileDOcument("calendarprofile", "") ), and the name is on an item named "Owner". So far, so good. It would be a good idea to make sure that the name you get is in "distinguished name" format (i.e., cn=xxx/o=yyy...)
    2.        Figure out if the curernt document is unread for the name we found in step 1. Unfortunately, there is no back-end class support for this functionality, you have to drop down to the C API. I knew how to do this in LotusScript, but not from .NET. Fortunately, some Google research found me a couple of articles that were very helpful (links at the end of this post). Knowing what C API calls to use, I was able to write a C# class to tell me whether a NOTEID is in the unread list or not.

    It turns out that the magic incantation in C# is easy to do, once you know it. You have to tell CLR ("common language runtime") where the DLL is that you want to access, and what the signature (call name and parameter types) of the entry point you need is. Just like Declare statements in LotusScript (with different syntax, of course).

    Here's an extract from my code showing the Notes entry points I needed:


          /*
           * STATUS LNPUBLIC NSFDbOpen(
              const char far *PathName,
              DBHANDLE far *rethDB);
          */
          [DllImport("nnotes.dll")]
          public static extern STATUS NSFDbOpen(String path, ref HANDLE phDB);

          /*
           * STATUS LNPUBLIC NSFDbClose(DBHANDLE  hDB);
          */
          [DllImport("nnotes.dll")]
          public static extern STATUS NSFDbClose(HANDLE hDb);

          /*
           * STATUS LNPUBLIC NSFDbGetUnreadNoteTable2(
              DBHANDLE  hDB,
              char far *UserName,
              WORD  UserNameLength,
              BOOL  fCreateIfNotAvailable,
              BOOL  fUpdateUnread,
              HANDLE far *rethUnreadList);
          */
          [DllImport("nnotes.dll")]
          public static extern STATUS NSFDbGetUnreadNoteTable2(
              HANDLE hDb, String user, ushort namelen, bool create, bool update, ref HANDLE hList);

          /*
           * STATUS LNPUBLIC NSFDbUpdateUnread(
                  DBHANDLE  hDataDB,
                  HANDLE  hUnreadList);
           */
          [DllImport("nnotes.dll")]
          public static extern STATUS NSFDbUpdateUnread(HANDLE hDb, HANDLE hUnreadList);

          /*
           * BOOL LNPUBLIC IDIsPresent(HANDLE  hTable, DWORD  id);
          */
          [DllImport("nnotes.dll")]
          public static extern bool IDIsPresent(HANDLE hTable, DWORD id);

          /*
           * STATUS LNPUBLIC OSMemFree(HANDLE  Handle);
          */
          [DllImport("nnotes.dll")]
          public static extern STATUS OSMemFree(HANDLE h);


    Note that every call is preceded by " [DllImport("nnotes.dll")]", that's a requirement (and the DLL must be on the system PATH). There's no "#include" directive in C# to bring in header files, so you have to map all the special Notes datatypes (like DBHANDLE, NOTEID, etc.) to native C# types. I did this with "using" statements, like this:

    using HANDLE = System.UInt32;
    using DWORD  = System.UInt32;
    using STATUS = System.UInt16;

    After doing this, I just called the routines as if they were native C# code. CLR took care of mapping string buffers to "unmanaged" memory and so on. It worked great! Since this post is already too long, I won't post the class I wrote to use these CAPI calls, but it's very simple. There's an Init() routine that takes a server, db and username, and opens the database. With the username, it gets the IDTable representing the user's unread list (and updates it). Then, every time I get a new Document, I convert it's NOTEID string to a number, and see if that ID is in the unread list. On shutdown I free the IDList and close the Database (actually, I could almost certainly close the database after retrieving the IDList....)

    Not bad! The articles I read said there was a way to invoke all kinds of entry points: using structs, passing callback functions, everything (someday I'm sure I'll need to do those things too, just didn't have to this time).

    Here are the 2 links I promised:
    http://msdn.microsoft.com/en-us/magazine/cc301501.aspx
    http://msdn.microsoft.com/en-us/library/ms123402.aspx (I swear this one was there on Saturday, but today when I went to verify it, I got "content not found". Oh well, keep trying...)

    Enjoy!


    (Need expert application development architecture/coding help? Contact me at: bbalaban, gmail.com)
    Follow me on Twitter @LooseleafLLC
    This article ┬ęCopyright 2009 by Looseleaf Software LLC, all rights reserved. You may link to this page, but may not copy without prior approval.
    Comments

    1Josep Alemany  5/6/2008 1:32:33 AM  Calling Notes CAPI from C#/Visual Studio

    Really interesting!

    I'm going to test it and post in my blog translated to spanish (if you are agree).

    Regards,

    Josep Alemany

    2Peter LaComb  5/6/2008 9:24:04 AM  Calling Notes CAPI from C#/Visual Studio

    And just when I was wanted to start digging in to the magic that was Proposion's N2N....

    3Dwight Wilbanks  5/8/2008 9:48:16 AM  Calling Notes CAPI from C#/Visual Studio

    When I was learning the CAPI calling syntax for lotusscript, I created a view in the reference database that formated the Declare lines using simple string manipulation. I don't know c# well enough to do it here, but, that idea might help someone.

    4Eve MacGregor  5/16/2008 5:45:01 PM  Calling Notes CAPI from C#/Visual Studio

    I'd really like to see what you did with the note (mail) items. I'm trying to retrieve the first item and associated value with NSFItemInfo - but I keep triggering a panic halt with OSLockObject when I try to setup accessing the content of the item's value. Obviously the handle is out of range - but ? Here is the snippet (with the marshalling parts left out)...

    #begin snippet

    BLOCKID itemBlockID = new BLOCKID();

    LotusNotes.TYPE type = LotusNotes.TYPE.TYPE_UNAVAILABLE;

    BLOCKID valueBlockID = new BLOCKID();

    int valueLength = 0;

    if (NSFItemInfo(hNote, null, 0, ref itemBlockID, ref type, ref valueBlockID, ref valueLength) == 0)

    {

    IntPtr valuehandle = valueBlockID.PoolHandle;

    //Directly using OSLock instead of the macro OSLockBlock

    IntPtr valuePtr = OSLockObject(valuehandle);

    ...

    }

    #endsnippet

    any suggestions would be appreciated. offline if you want.

    thanks,

    eve

    5Bob Balaban  5/17/2008 3:27:08 AM  Calling Notes CAPI from C#/Visual Studio

    @4 - Interesting problem. How do you define the BLOCKID struct? I don't think I would use IntPtr for the HANDLE part of a BLOCKID, try using a DWORD equivalent (UInt32 is what I used).

    Using OSLockObject directly is probably a good idea (I haven't tried it myself yet), but you need to add the Block part of the BLOCKID struct to the result, as the macro does (check pool.h).

    But I guess my biggest question is, what do you do with the valuePtr once you get it (assuming you work around the crash problem...)? Wouldn't you be better off using C++?

    6Eve  5/20/2008 11:19:57 AM  Calling Notes CAPI from C#/Visual Studio

    I realized after I posted this that it was missing one line - the one where I added the Block part of the BLOCKID to the pool (which I defined as follows)

    [StructLayout(LayoutKind.Sequential)]

    public struct BLOCKID

    { public IntPtr PoolHandle;

    public ushort BlockHandle; }

    I think I've tried using UInt32 for the PoolHandle - but I'll try again because I've forgotten... sigh. The idea is to use valuePtr to get the "value" of the item. More specifically - I'm trying to retrieve all of the items of a email message. Essentially it's a Lotus email export function via a drag and drop action. I'm using c# because that's the language of choice around here - though over the weekend I decided I'd just try c.

    Let me know if you have any other thoughts... Thanks for having tried this stuff first. I had a great (!) long weekend.

    7Eve  5/20/2008 11:23:37 AM  Calling Notes CAPI from C#/Visual Studio

    My only other questions is if you didn't use OSLockObject to lock the items you wanted to access - what did you use?

    8Bob Balaban  5/21/2008 2:38:20 AM  Calling Notes CAPI from C#/Visual Studio

    @6, @7 - At this point, if you can use C you'd probably be better off. Once you start in with serious pointer manipulations, IMHO c# gets to be more trouble than it's worth.

    To your second question: when you have a BLOCKID, you want to use OSLockBlock (and OSUnlockBlock) to convert to a buffer pointer.

    These are macros defined in pool.h. The "lock" operation uses OSLockObject to deal with the ".pool" part of the BLOCKID, then adds the ".block" portion of the struct as an offset. Often the offset is a 0, but not always.

    9Dave Philips  7/1/2009 10:17:27 AM  Calling Notes CAPI from C#/Visual Studio

    Any ideas on calling OSPathNetConstruct from C#? I just get a blank string returned every time.

    10Bob Balaban  7/1/2009 11:57:15 AM  Calling Notes CAPI from C#/Visual Studio

    Did you mark the output parameter as "ref"?

    11Dave Philips  7/2/2009 4:07:52 AM  Calling Notes CAPI from C#/Visual Studio

    Hi Bob,

    Yes, I have placed ref infront of the parameter.

    Below is my current definition:

    [DllImport("nnotes.dll", CallingConvention = CallingConvention.StdCall, SetLastError = true, EntryPoint = "OSPathNetConstruct")]

    public static extern STATUS OSPathNetConstruct(long PortName, string ServerName, string FileName, ref StringBuilder sbPathName);

    Then I call it using the following:

    StringBuilder sb = new StringBuilder(256);

    ushort result = OSPathNetConstruct(0, ServerName, FileName, ref sb);

    No error codes are returned (I also check the Win32Exception). The odd thing is that the StringBuilder's size has been reset to zero. So it is definately doing something, just not what its supposed to.

    I've mapped into this function before in delphi using pchars, and that all works. I just can't seem to do the same in C#.

    12Dave Philips  7/2/2009 4:25:03 AM  Calling Notes CAPI from C#/Visual Studio

    I forgot to mention, both ServerName and FileName have the correct info in them beforehand. Also, you may notice that the PortName is defined as a long. I tried passing a null string instead, but the call exceptions. Then I found some LotusScript definitions for the function, and it defined the PortName as a long, and passed a value of 0.

    13Bob Balaban  7/2/2009 5:57:12 AM  Calling Notes CAPI from C#/Visual Studio

    Have you tried passing an array of chars, instead of an object?

    14Dave Philips  7/2/2009 6:51:48 AM  Calling Notes CAPI from C#/Visual Studio

    Yes, I've tried that too. The char array also gets its size reset, but gets a ushort placed in the first element? All very strange.

    I've even tried wrapping the char array in a GCHandle, and passing that to the function as follows:

    char[] chr = new char[256];

    gch = GCHandle.Alloc(chr);

    OSPathNetConstruct(0, ServerName, FileName, (IntPtr) gch);

    I'm at a loss as to what to do next.

    15Bob Balaban  7/3/2009 5:24:42 AM  Calling Notes CAPI from C#/Visual Studio

    I looked at this page and found an example:

    http://msdn.microsoft.com/en-us/magazine/cc301501.aspx

    using System.Text; // for StringBuilder

    public class Win32 {

    [DllImport("user32.dll")]

    public static extern int GetWindowText(int hwnd,

    StringBuilder buf, int nMaxCount);

    }

    int hwnd = // get it...

    StringBuilder cb = new StringBuilder(256);

    Win32.GetWindowText(hwnd, sb, sb.Capacity);

    He's pre-allocating the StringBuilder, and NOT using "ref" on the declaration.

    16Dave Philips  7/6/2009 5:15:12 AM  Calling Notes CAPI from C#/Visual Studio

    Hi Bob,

    I've tried that also, but to no avail. I'm totally stuck as to what to do. I think I might have to abandon trying to access this function via C#, and instead use my current delphi dll as a bridge and then pass the results from the procedure calls to my C# dll.

    17Heinz Wolek  7/19/2009 3:06:59 AM  Calling Notes CAPI from C#/Visual Studio

    HI Bob,

    I am completly new to C# and your article was the only source to get me started with access to lotus notes.

    But I have a problem:

    I am woking with VC# and it does not recognize STATUS NULLHANDLE.

    So I changed the code to:

    using System;

    using System.Collections.Generic;

    using System.Linq;

    using System.Text;

    // Sample program to call unmanaged code

    using System.Runtime.InteropServices;

    using DBHANDLE=System.UInt32;

    using HANDLE=System.UInt32;

    class PInvoke1App

    {

    /*

    * STATUS LNPUBLIC NSFDbOpen(

    const char far *PathName,

    DBHANDLE far *rethDB);

    */

    [DllImport("nnotes.dll")]

    public static extern void NSFDbOpen(String path, ref HANDLE phDB);

    public static void Main()

    {

    HANDLE db_handle; /* database handle */

    db_handle = NULLHANDLE;

    try

    {

    NSFDbOpen("c:\\lotus\\notes\\data\\bookmark.nsf", ref db_handle);

    }

    catch (AccessViolationException e)

    {

    Console.WriteLine("Fehler----------: " + e.ToString());

    }

    }

    }

    It compiles without error but when I run it I get an AccessViolationException.

    Could you give me a hint?

    18Heinz Wolek  7/19/2009 3:08:58 AM  Calling Notes CAPI from C#/Visual Studio

    Sorry,

    I changed the line

    db_handle = NULLHANDLE;

    to

    db_handle = 0;

    19Bob Balaban  7/19/2009 4:51:17 PM  Calling Notes CAPI from C#/Visual Studio

    Just use NULL instead of NULLHANDLE, should work fine

    20a  11/15/2009 10:56:40 PM  Calling Notes CAPI from C#/Visual Studio

    using DBHANDLE=System.UInt32;

    Uint32, not nullable, how to "NULL instead of NULLHANDLE,"?

    21Bob Balaban  11/16/2009 5:12:05 PM  Calling Notes CAPI from C#/Visual Studio

    You can just use 0

    or make NULL a constant with value 0

    22Jignesh  3/8/2013 3:53:40 AM  Calling Notes CAPI from C-sharp/Visual Studio

    Hi bob,

    Do you have any idea to solve exception saying:

    "A first chance exception of type 'System.AccessViolationException' occurred in UsingCAPI.exe

    Additional information: Attempted to read or write protected memory. This is often an indication that other memory is corrupt.

    "

    23Bob Balaban  3/16/2013 11:34:08 AM  Calling Notes CAPI from C-sharp/Visual Studio

    Hi, usually that means you are passing a bad handle or pointer to the C entry point

    24Kyle Hamilton  7/14/2013 8:38:01 PM  Calling Notes CAPI from C-sharp/Visual Studio

    [DllImport("notes.dll",EntryPoint="NotesInitExtended")]

    extern static STATUS NotesInitExtended(int argc, [MarshalAs(UnmanagedType.LPArray,ArraySubType=UnmanagedType.LPStr)] string[] argv);

    25John  11/12/2014 8:26:28 AM  Calling Notes CAPI from C-sharp/Visual Studio

    Hi Bob,

    Thank you for the great article .

    I am however getting an System.AccessViolationException was unhandled error.

    when I am trying to call the LotusNotes.NotesEntries.NSFNoteOpenExt(hCC, nid,

    LotusNotes.NotesEntries.OPEN_RAW_MIME, ref hNote)

    in the ConvertToMIME(string noteid, NotesSession session, NotesDatabase nsf) function.

    I have tried setting the handle to null or 0 but does not work.

    I have pasted the code below.

    using System;

    using System.IO;

    using System.Collections.Generic;

    using System.Text;

    using Domino; // Domino COM classes

    using HANDLE = System.UInt32;

    using DWORD = System.UInt32;

    using STATUS = System.UInt16;

    using WORD = System.UInt16;

    using TIMEDATE = System.UInt64; // equivalent to DWORD[2]

    using BOOL = System.UInt16;

    public struct ENCRYPTION_KEY

    {

    public byte Byte1;

    public WORD Word1;

    //BYTE Text[16];

    public DWORD dw1;

    public DWORD dw2;

    };

    namespace LotusNotes

    {

    class Program

    {

    // constants

    public static string Copyright =

    "Copyright 2009 Looseleaf Software LLC, All rights reserved.";

    private static string OUTPUT_FOLDER = @"c:\Test";

    public static WORD NULL = 0;

    public static WORD UPDATE_FORCE = 1;

    private static BOOL FALSE = 0;

    private static WORD DECRYPT_ATTACHMENTS_IN_PLACE = 1;

    // "global" to the class, so we don't have to keep opening it:

    // handle to conversion database for use in CAPI

    private HANDLE m_hDb = NULL;

    static void Main(string[] args)

    {

    Program p = new Program();

    p.RealMain(args);

    } // end main

    private void RealMain(string[] args)

    {

    int count = 0;

    int i;

    STATUS stat = 0;

    Domino.NotesSession session = null;

    Domino.NotesDatabase nsf = null;

    NotesSession ns = new NotesSession();

    NotesView nv;

    NotesDatabase nd;

    NotesDocument ndoc;

    NotesDocument ndocNext;

    // string database = args[1];

    //if (database.Length == 0)

    //{

    // Console.WriteLine("Conversion database name must be supplied");

    // goto XEnd;

    //}

    // init Notes back-end

    // acquire your list of documents to convert here

    if (ns != null)

    {

    ns.Initialize("");

    nd = ns.GetDatabase("Server//Int", "maili.nsf", false);

    session = new NotesSession();

    session.Initialize(null);

    session.ConvertMime = false;

    // find the nsf

    //nsf = session.GetDatabase("", database, false);

    //if (nsf == null || !nsf.IsOpen)

    //{

    // Console.WriteLine("ERROR: Database " + database + " not found");

    // goto XEnd;

    //}

    //stat = LotusNotes.NotesEntries.NSFDbOpen(database, ref this.m_hDb);

    if (nd != null)

    {

    nv = nd.GetView("Test");

    if (nv != null)

    {

    ndoc = nv.GetFirstDocument();

    while (ndoc != null)

    {

    ndocNext = nv.GetNextDocument(ndoc);

    if (ConvertOneDoc(ns, nd, ndoc.NoteID.ToString()))

    ndoc = ndocNext;

    }

    }

    }

    }

    //for (i = 0; i < filelist.Length; i++)

    //{

    // if (ConvertOneDoc(session, nsf, filelist))

    // count++;

    //} // end for

    XEnd:

    Console.WriteLine(count + " documents converted");

    Console.Write("\nPress ENTER to continue: ");

    Console.Read();

    if (nsf != null)

    nsf = null;

    if (session != null)

    session = null;

    if (this.m_hDb != NULL)

    LotusNotes.NotesEntries.NSFDbClose(this.m_hDb);

    } // end RealMain

    // convert one dxl file to mime

    private bool ConvertOneDoc(Domino.NotesSession session,

    Domino.NotesDatabase nsf,

    string noteid)

    {

    string outfile=string.Empty;

    bool result = false;

    ConvertToMIME(noteid, session, nsf);

    result = WriteMIMEOutput(outfile, noteid, nsf, session);

    return result;

    } // end ConvertOneFile

    private void ConvertToMIME(string noteid, NotesSession session, NotesDatabase nsf)

    {

    //string result = null;

    HANDLE hNote = NULL;

    HANDLE hCC = NULL;

    string path = nsf.FilePath;

    STATUS stat = 0;

    HANDLE hID = NULL;

    // TIMEDATE until = 0;

    uint nid = 0;

    string kprivate = "$KeepPrivate";

    // get the new document's noteid -

    nid = Convert.ToUInt32(noteid, 16);

    stat = LotusNotes.NotesEntries.NSFNoteOpenExt(this.m_hDb, nid,

    LotusNotes.NotesEntries.OPEN_RAW_MIME, ref hNote);

    if (stat > 0)

    goto XEnd;

    // if the note is encrypted, try to decrypt it. If that fails

    // (e.g., we don't have the key), then we can't convert to MIME

    // (we don't care about the signature)

    BOOL isSealed = 0, isSigned = 0;

    LotusNotes.NotesEntries.NSFNoteIsSignedOrSealed(hNote, ref isSigned, ref isSealed);

    if (isSealed != 0)

    {

    ENCRYPTION_KEY key;

    key.Byte1 = 0; key.dw1 = 0; key.dw2 = 0; key.Word1 = 0;

    stat = LotusNotes.NotesEntries.NSFNoteDecrypt(hNote, DECRYPT_ATTACHMENTS_IN_PLACE, ref key);

    if (stat != 0)

    {

    Console.WriteLine("ERROR: Document note id " + noteid + " is encrypted, cannot be converted.");

    goto XEnd;

    }

    } // end isSealed

    // If present, $KeepPrivate will prevent conversion, so nuke the sucka

    stat = LotusNotes.NotesEntries.NSFItemDelete(hNote, kprivate, (ushort)kprivate.Length);

    // if the note is already in mime format, we don't have to convert

    if (LotusNotes.NotesEntries.NSFNoteHasMIMEPart(hNote) == 0 ||

    LotusNotes.NotesEntries.NSFNoteHasMIME(hNote) == 0)

    {

    LotusNotes.NotesEntries.MMCreateConvControls(ref hCC);

    LotusNotes.NotesEntries.MMSetMessageContentEncoding(hCC, 2); // html w/images & attachments

    stat = LotusNotes.NotesEntries.MIMEConvertCDParts(hNote, FALSE, FALSE, hCC);

    if (stat == 0)

    stat = LotusNotes.NotesEntries.NSFNoteUpdate(hNote, UPDATE_FORCE);

    LotusNotes.NotesEntries.MMDestroyConvControls(hCC);

    } // end not-mime

    XEnd:

    if (hNote != NULL)

    LotusNotes.NotesEntries.NSFNoteClose(hNote);

    if (hID != NULL)

    LotusNotes.NotesEntries.IDDestroyTable(hID);

    //return result;

    } // end ExportNewDoc

    private static bool WriteMIMEOutput(string outfile, string noteid, NotesDatabase nsf,

    NotesSession session)

    {

    Domino.NotesMIMEEntity mE = null;

    Domino.NotesMIMEEntity mChild = null;

    bool result = false;

    string contentType = null;

    string headers = null;

    string content = null;

    string preamble = null;

    MIME_ENCODING encoding;

    StreamWriter writer = new StreamWriter(outfile);

    NotesDocument doc = nsf.GetDocumentByID(noteid);

    if (doc == null)

    goto XEnd;

    mE = doc.GetMIMEEntity("body");

    if (mE == null)

    goto XEnd;

    contentType = mE.ContentType;

    headers = mE.Headers;

    encoding = mE.Encoding;

    // message envelope. If no MIME-Version header, add one

    if (!headers.Contains("MIME-Version:"))

    writer.WriteLine("MIME-Version: 1.0");

    writer.WriteLine(headers);

    // for multipart, usually no main-msg content...

    content = mE.ContentAsText;

    if (content != null && content.Trim().Length > 0)

    writer.WriteLine(content);

    writer.Flush();

    if (contentType.StartsWith("multipart"))

    {

    preamble = mE.Preamble;

    mChild = mE.GetFirstChildEntity();

    while (mChild != null)

    {

    headers = mChild.Headers;

    encoding = mChild.Encoding;

    // if it's a binary part, force it to b64

    if (encoding == MIME_ENCODING.ENC_IDENTITY_BINARY)

    {

    mChild.EncodeContent(MIME_ENCODING.ENC_BASE64);

    headers = mChild.Headers;

    }

    preamble = mChild.Preamble;

    content = mChild.BoundaryStart;

    writer.Write(content);

    if (!content.EndsWith("\n"))

    writer.WriteLine("");

    writer.WriteLine(headers);

    writer.WriteLine();

    content = mChild.ContentAsText;

    writer.Write(content);

    writer.Write(mChild.BoundaryEnd);

    writer.Flush();

    mChild = mChild.GetNextSibling();

    } // end while

    } // end multipart

    // end of main envelope

    writer.WriteLine(mE.BoundaryEnd);

    result = true;

    XEnd:

    if (writer != null)

    writer.Close();

    return result;

    } // end WriteMIMEOutput

    }

    }