Mapi.cs
author Thomas
Mon, 25 Mar 2019 13:27:16 +0100
branchsync
changeset 2610 09fde2338362
parent 2597 a745e942a666
child 3288 3c10b308e3c3
permissions -rw-r--r--
Merge with default

´╗┐using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Outlook = Microsoft.Office.Interop.Outlook;

namespace pEp
{
    /// <summary>
    /// Provides methods to interact with MAPI via native (p/invoke) calls.
    /// </summary>
    internal class Mapi
    {
        #region Definitions
        /// <summary>
        /// A collection of MAPI error or warning codes.
        /// </summary>
        internal enum HResult : uint
        {
            S_FALSE                             = 0x00000001,
            S_OK                                = 0x00000000,

            E_NOTIMPL                           = 0x80004001,

            MAPI_E_INTERFACE_NOT_SUPPORTED	    = 0x80004002,
            MAPI_E_INTERFACE_NOT_SUPPORTED_2    = 0x80000004,
            MAPI_E_CALL_FAILED                  = 0x80004005,
            MAPI_E_NOT_ENOUGH_MEMORY            = 0x8007000E,
            MAPI_E_INVALID_PARAMETER            = 0x80000003,
            MAPI_E_NO_ACCESS                    = 0x80070005,
            MAPI_E_NO_ACCESS_2                  = 0x80000009,

            E_INVALIDARG                        = 0x80070057,
            E_OUTOFMEMORY                       = 0x80000002,
            E_UNEXPECTED                        = 0x8000FFFF,
            E_FAIL                              = 0x80000008,

            MAPI_E_NO_SUPPORT                   = 0x80040000 | 0x102,
            MAPI_E_BAD_CHARWIDTH                = 0x80040000 | 0x103,
            MAPI_E_STRING_TOO_LONG              = 0x80040000 | 0x105,
            MAPI_E_UNKNOWN_FLAGS                = 0x80040000 | 0x106,
            MAPI_E_INVALID_ENTRYID              = 0x80040000 | 0x107,
            MAPI_E_INVALID_OBJECT               = 0x80040000 | 0x108,
            MAPI_E_OBJECT_CHANGED               = 0x80040000 | 0x109,
            MAPI_E_OBJECT_DELETED               = 0x80040000 | 0x10A,
            MAPI_E_BUSY                         = 0x80040000 | 0x10B,
            MAPI_E_NOT_ENOUGH_DISK              = 0x80040000 | 0x10D,
            MAPI_E_NOT_ENOUGH_RESOURCES         = 0x80040000 | 0x10E,
            MAPI_E_NOT_FOUND                    = 0x80040000 | 0x10F,
            MAPI_E_VERSION                      = 0x80040000 | 0x110,
            MAPI_E_LOGON_FAILED                 = 0x80040000 | 0x111,
            MAPI_E_SESSION_LIMIT                = 0x80040000 | 0x112,
            MAPI_E_USER_CANCEL                  = 0x80040000 | 0x113,
            MAPI_E_UNABLE_TO_ABORT              = 0x80040000 | 0x114,
            MAPI_E_NETWORK_ERROR                = 0x80040000 | 0x115,
            MAPI_E_DISK_ERROR                   = 0x80040000 | 0x116,
            MAPI_E_TOO_COMPLEX                  = 0x80040000 | 0x117,
            MAPI_E_BAD_COLUMN                   = 0x80040000 | 0x118,
            MAPI_E_EXTENDED_ERROR               = 0x80040000 | 0x119,
            MAPI_E_COMPUTED                     = 0x80040000 | 0x11A,
            MAPI_E_CORRUPT_DATA                 = 0x80040000 | 0x11B,
            MAPI_E_UNCONFIGURED                 = 0x80040000 | 0x11C,
            MAPI_E_FAILONEPROVIDER              = 0x80040000 | 0x11D,
            MAPI_E_UNKNOWN_CPID                 = 0x80040000 | 0x11E,
            MAPI_E_UNKNOWN_LCID                 = 0x80040000 | 0x11F,

            MAPI_E_CORRUPT_STORE                = 0x80040000 | 0x600,
            MAPI_E_NOT_IN_QUEUE                 = 0x80040000 | 0x601,
            MAPI_E_NO_SUPPRESS                  = 0x80040000 | 0x602,
            MAPI_E_COLLISION                    = 0x80040000 | 0x604,
            MAPI_E_NOT_INITIALIZED              = 0x80040000 | 0x605,
            MAPI_E_NON_STANDARD                 = 0x80040000 | 0x606,
            MAPI_E_NO_RECIPIENTS                = 0x80040000 | 0x607,
            MAPI_E_SUBMITTED                    = 0x80040000 | 0x608,
            MAPI_E_HAS_FOLDERS                  = 0x80040000 | 0x609,
            MAPI_E_HAS_MESSAGES                 = 0x80040000 | 0x60A,
            MAPI_E_FOLDER_CYCLE                 = 0x80040000 | 0x60B
        }

        /// <summary>
        /// Interface IDs used to retrieve the specific MAPI Interfaces from the IUnknown object.
        /// </summary>
        internal static class MAPIInterfaceIds
        {
            public const string IMAPISession        = "00020300-0000-0000-C000-000000000046";
            public const string IMAPIProp           = "00020303-0000-0000-C000-000000000046";
            public const string IMAPITable          = "00020301-0000-0000-C000-000000000046";
            public const string IMAPIMsgStore       = "00020306-0000-0000-C000-000000000046";
            public const string IMAPIFolder         = "0002030C-0000-0000-C000-000000000046";
            public const string IMAPISpoolerService = "0002031E-0000-0000-C000-000000000046";
            public const string IMAPIStatus         = "0002031E-0000-0000-C000-000000000046";
            public const string IMessage            = "00020307-0000-0000-C000-000000000046";
            public const string IAddrBook           = "00020309-0000-0000-C000-000000000046";
            public const string IProfSect           = "00020304-0000-0000-C000-000000000046";
            public const string IMAPIContainer      = "0002030B-0000-0000-C000-000000000046";
            public const string IABContainer        = "0002030D-0000-0000-C000-000000000046";
            public const string IMsgServiceAdmin    = "0002031D-0000-0000-C000-000000000046";
            public const string IProfAdmin          = "0002031C-0000-0000-C000-000000000046";
            public const string IMailUser           = "0002030A-0000-0000-C000-000000000046";
            public const string IDistList           = "0002030E-0000-0000-C000-000000000046";
            public const string IAttachment         = "00020308-0000-0000-C000-000000000046";
            public const string IMAPIControl        = "0002031B-0000-0000-C000-000000000046";
            public const string IMAPILogonRemote    = "00020346-0000-0000-C000-000000000046";
            public const string IMAPIForm           = "00020327-0000-0000-C000-000000000046";

            public const string IID_IStorage        = "0000000B-0000-0000-C000-000000000046";
            public const string IID_IStream         = "0000000C-0000-0000-C000-000000000046";
        }

        /// <summary>
        /// Save options for the IMAPIProp.SaveChanges method.
        /// </summary>
        internal enum SaveOption
        {
            KEEP_OPEN_READONLY = 0x00000001,
            KEEP_OPEN_READWRITE = 0x00000002,
            FORCE_SAVE = 0x00000004
        }

        /// <summary>
        /// Deletion options for the IMAPIFolder.DeleteMessages method.
        /// </summary>
        internal enum DeletionFlags : uint
        {
            DELETE_HARD_DELETE = 0x00000010,
            //MESSAGE_DIALOG = ?
        }

        /// <summary>
        /// Flag to indicate that IMAPIProps::GetProps should return unicode values.
        /// </summary>
        private const uint MAPI_UNICODE = 0x80000000;

        #endregion

        #region Interfaces

        /// <summary>
        /// Manages high-level operations on container objects such as address books, distribution lists, and folders.
        /// </summary>
        [
            ComImport,
            ComVisible(false),
            InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
            Guid(Mapi.MAPIInterfaceIds.IMAPIContainer)
        ]
        internal interface IMAPIContainer : IMAPIProp
        {
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetContentsTable();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetHierarchyTable();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int OpenEntry(uint cbEntryId, IntPtr entryId, ref Guid iid, uint flags, out IntPtr type, out IntPtr iUnk);
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int SetSearchCriteria();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetSearchCriteria();
        }

        /// <summary>
        /// Performs operations on the messages and subfolders in a folder.
        /// </summary>
        [
            ComImport,
            ComVisible(false),
            InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
            Guid(Mapi.MAPIInterfaceIds.IMAPIFolder)
        ]
        internal interface IMAPIFolder : IMAPIContainer
        {
            /// <summary>
            /// Creates a new message.
            /// <paramref name="_interface"/>A pointer to the interface identifier (IID) that represents the interface 
            /// to be used to access the new message. Valid interface identifiers include IID_IUnknown, IID_IMAPIProp, 
            /// IID_IMAPIContainer, and IID_IMAPIFolder. Passing NULL causes the message store provider to return the 
            /// standard message interface, IMessage::IMAPIProp.</param>
            /// <param name="flags">A bitmask of flags that controls how the message is created.</param>
            /// <param name="message">A pointer to a pointer to the newly created message.</param>
            /// See: https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/imapifolder-createmessage
            /// </summary>
            /// <returns>The status of this method.</returns>
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int CreateMessage(ref Guid lpiid, uint flags, [MarshalAs(UnmanagedType.Interface)] out IMessage message);

            /// <summary>
            /// Copies or moves one or more messages. 
            /// </summary>
            /// <returns>The status of this method.</returns>
            int CopyMessages();

            /// <summary>
            /// Deletes one or more messages. 
            /// </summary>
            /// <param name="msgList">A pointer to an ENTRYLIST structure that contains the 
            /// number of messages to delete and an array of ENTRYID structures that identify the messages.</param>
            /// <param name="uiParam">A handle to the parent window of the progress indicator. The ulUIParam 
            /// parameter is ignored unless the MESSAGE_DIALOG flag is set in the ulFlags parameter.</param>
            /// <param name="progress">A pointer to a progress object that displays a progress indicator. 
            /// If IntPtr.Zero is passed in lpProgress, the message store provider displays a progress indicator 
            /// by using the MAPI progress object implementation. The lpProgress parameter is ignored unless the 
            /// MESSAGE_DIALOG flag is set in the ulFlags parameter.</param>
            /// <param name="flags">A bitmask of flags that controls how the messages are deleted. The following flags can be set:
            /// DELETE_HARD_DELETE: Permanently removes all messages, including soft-deleted ones.
            /// MESSAGE_DIALOG: Displays a progress indicator as the operation proceeds.</param>
            /// See: https://docs.microsoft.com/en-us/office/client-developer/outlook/mapi/imapifolder-deletemessages
            /// <returns>The status of this method.</returns>
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int DeleteMessages(IntPtr msgList, uint uiParam, IntPtr progress, uint flags);

            /// <summary>
            /// Creates a new subfolder. 
            /// </summary>
            /// <returns>The status of this method.</returns>
            int CreateFolder();

            /// <summary>
            /// Copies or moves a subfolder. 
            /// </summary>
            /// <returns>The status of this method.</returns>
            int CopyFolder();

            /// <summary>
            /// Deletes a subfolder. 
            /// </summary>
            /// <returns>The status of this method.</returns>
            int DeleteFolder();

            /// <summary>
            /// Sets or clears the MSGFLAG_READ flag in the PR_MESSAGE_FLAGS (PidTagMessageFlags) property 
            /// of one or more of the folder's messages, and manages the sending of read reports.
            /// </summary>
            /// <returns>The status of this method.</returns>
            int SetReadFlags();

            /// <summary>
            /// Obtains the status associated with a message in a particular folder.
            /// </summary>
            /// <returns>The status of this method.</returns>
            int GetMessageStatus();

            /// <summary>
            ///  Sets the status associated with a message.
            /// </summary>
            /// <returns>The status of this method.</returns>
            int SetMessageStatus();

            /// <summary>
            /// Sets the default sort order for a folder's contents table.
            /// </summary>
            /// <returns>The status of this method.</returns>
            int SaveContentsSort();

            /// <summary>
            ///  Deletes all messages and subfolders from a folder without deleting the folder itself. 
            /// </summary>
            /// <returns>The status of this method.</returns>
            int EmptyFolder();
        }


        /// <summary>
        /// Enables clients, service providers, and MAPI to work with properties. All objects that 
        /// support properties implement this interface.
        /// </summary>
        [
            ComImport,
            ComVisible(false),
            InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
            Guid(Mapi.MAPIInterfaceIds.IMAPIProp)
        ]
        internal interface IMAPIProp
        {
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetLastError();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int SaveChanges(uint uFlags);
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetProps([MarshalAs(UnmanagedType.LPArray)] uint[] lpPropTagArray, uint flags, out uint count, out IntPtr propArray);
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetPropList();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int OpenProperty(uint ulPropTag, ref Guid lpiid, uint ulInterfaceOptions, uint ulFlags, out IntPtr lppUnk);
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int SetProps(uint values, IntPtr propArray, IntPtr problems);
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int DeleteProps();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int CopyTo();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int CopyProps();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetNamesFromIDs();
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int GetIDsFromNames();
        }

        /// <summary>
        /// Provides a read-only view of a table. IMAPITable is used by clients and service providers to manipulate the way a table appears. 
        /// </summary>
        [Guid("00020301-0000-0000-c000-000000000046"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IMAPITable
        {
            /// <summary>
            /// Returns a MAPIERROR structure containing information about the previous error on the table.
            /// </summary>
            /// <param name="hResult">HRESULT containing the error generated in the previous method call.</param>
            /// <param name="ulFlags">Bitmask of flags that controls the type of the returned strings. </param>
            /// <param name="lppMAPIError">Pointer to a pointer to the returned MAPIERROR structure containing version, component, and context information for the error.</param>
            /// <returns>S_OK, if the call succeeded and has returned the expected value or values; otherwise, failed.</returns>
            int GetLastError(int hResult, uint ulFlags, out IntPtr lppMAPIError);
            /// <summary>
            /// Registers an advise sink object to receive notification of specified events affecting the table.
            /// </summary>
            /// <param name="ulEventMask">Value indicating the type of event that will generate the notification.</param>
            /// <param name="lpAdviseSink">Pointer to an advise sink object to receive the subsequent notifications. This advise sink object must have been already allocated.</param>
            /// <param name="lpulConnection">Pointer to a nonzero value that represents the successful notification registration.</param>
            /// <returns>S_OK, if the notification registration successfully completed; otherwise, failed.</returns>
            int Advise(uint ulEventMask, IntPtr lpAdviseSink, IntPtr lpulConnection);
            /// <summary>
            /// Cancels the sending of notifications previously set up with a call to the IMAPITable::Advise method.
            /// </summary>
            /// <param name="ulConnection">The number of the registration connection returned by a call to IMAPITable::Advise.</param>
            /// <returns>S_OK, if the call succeeded; otherwise, failed.</returns>
            int Unadvise(uint ulConnection);
            /// <summary>
            /// Returns the table's status and type.
            /// </summary>
            /// <param name="lpulTableStatus">Pointer to a value indicating the status of the table.</param>
            /// <param name="lpulTableType">Pointer to a value that indicates the table's type.</param>
            /// <returns>S_OK, if the table's status was successfully returned; otherwise, failed.</returns>
            int GetStatus(IntPtr lpulTableStatus, IntPtr lpulTableType);
            /// <summary>
            /// Defines the particular properties and order of properties to appear as columns in the table.
            /// </summary>
            /// <param name="lpPropTagArray">Pointer to an array of property tags identifying properties to be included as columns in the table. </param>
            /// <param name="ulFlags">Bitmask of flags that controls the return of an asynchronous call to SetColumns.</param>
            /// <returns>S_OK, if the column setting operation was successful; otherwise, failed.</returns>
            int SetColumns([MarshalAs(UnmanagedType.LPArray)] uint[] lpPropTagArray, uint ulFlags);
            /// <summary>
            /// Returns a list of columns for the table.
            /// </summary>
            /// <param name="ulFlags">Bitmask of flags that indicates which column set should be returned.</param>
            /// <param name="lpPropTagArray">Pointer to an SPropTagArray structure containing the property tags for the column set.</param>
            /// <returns>S_OK, if the column set was successfully returned; otherwise, failed.</returns>
            int QueryColumns(uint ulFlags, IntPtr lpPropTagArray);
            /// <summary>
            /// Returns the total number of rows in the table. 
            /// </summary>
            /// <param name="ulFlags">Reserved; must be zero.</param>
            /// <param name="lpulCount">Pointer to the number of rows in the table.</param>
            /// <returns>S_OK, if the row count was successfully returned; otherwise, failed.</returns>
            int GetRowCount(uint ulFlags, out uint lpulCount);
            /// <summary>
            /// Moves the cursor to a specific position in the table.
            /// </summary>
            /// <param name="bkOrigin">The bookmark identifying the starting position for the seek operation.</param>
            /// <param name="lRowCount">The signed count of the number of rows to move, starting from the bookmark identified by the bkOrigin parameter.</param>
            /// <param name="lplRowsSought">If lRowCount is a valid pointer on input, lplRowsSought points to the number of rows that were processed in the seek operation, the sign of which indicates the direction of search, forward or backward. If lRowCount is negative, then lplRowsSought is negative.</param>
            /// <returns>S_OK, if the seek operation was successful; otherwise, failed.</returns>
            int SeekRow(int bkOrigin, int lRowCount, out IntPtr lplRowsSought);
            /// <summary>
            /// Moves the cursor to an approximate fractional position in the table. 
            /// </summary>
            /// <param name="ulNumerator">The numerator of the fraction representing the table position</param>
            /// <param name="ulDenominator">The denominator of the fraction representing the table position</param>
            /// <returns>S_OK, if the seek operation was successful; otherwise, failed.</returns>
            int SeekRowApprox(uint ulNumerator, uint ulDenominator);
            /// <summary>
            /// Retrieves the current table row position of the cursor, based on a fractional value.
            /// </summary>
            /// <param name="lpulRow">Pointer to the number of the current row.</param>
            /// <param name="lpulNumerator">Pointer to the numerator for the fraction identifying the table position.</param>
            /// <param name="lpulDenominator">Pointer to the denominator for the fraction identifying the table position.</param>
            /// <returns>S_OK, if the method returned valid values in lpulRow, lpulNumerator, and lpulDenominator; otherwise, failed.</returns>
            int QueryPosition(IntPtr lpulRow, IntPtr lpulNumerator, IntPtr lpulDenominator);
            /// <summary>
            /// Finds the next row in a table that matches specific search criteria and moves the cursor to that row.
            /// </summary>
            /// <param name="lpRestriction">A pointer to an SRestriction structure that describes the search criteria.</param>
            /// <param name="BkOrigin">A bookmark identifying the row where FindRow should begin its search.</param>
            /// <param name="ulFlags">A bitmask of flags that controls the direction of the search.</param>
            /// <returns>S_OK, if the find operation was successful; otherwise, failed.</returns>
            int FindRow(out IntPtr lpRestriction, uint BkOrigin, uint ulFlags);
            /// <summary>
            /// Applies a filter to a table, reducing the row set to only those rows matching the specified criteria.
            /// </summary>
            /// <param name="lpRestriction">Pointer to an SRestriction structure defining the conditions of the filter. Passing NULL in the lpRestriction parameter removes the current filter.</param>
            /// <param name="ulFlags">Bitmask of flags that controls the timing of the restriction operation.</param>
            /// <returns>S_OK, if the filter was successfully applied; otherwise, failed.</returns>
            int Restrict(out IntPtr lpRestriction, uint ulFlags);
            /// <summary>
            /// Creates a bookmark at the table's current position.
            /// </summary>
            /// <param name="lpbkPosition">Pointer to the returned 32-bit bookmark value. This bookmark can later be passed in a call to the IMAPITable::SeekRow method</param>
            /// <returns>S_OK, if the call succeeded and has returned the expected value or values; otherwise, failed.</returns>
            int CreateBookmark(IntPtr lpbkPosition);
            /// <summary>
            /// Releases the memory associated with a bookmark.
            /// </summary>
            /// <param name="bkPosition">The bookmark to be freed, created by calling the IMAPITable::CreateBookmark method.</param>
            /// <returns>S_OK, if the bookmark was successfully freed; otherwise, failed.</returns>
            int FreeBookmark(IntPtr bkPosition);
            /// <summary>
            /// Orders the rows of the table, depending on sort criteria.
            /// </summary>
            /// <param name="lpSortCriteria">Pointer to an SSortOrderSet structure that contains the sort criteria to apply.</param>
            /// <param name="ulFlags">Bitmask of flags that controls the timing of the IMAPITable::SortTable operation.</param>
            /// <returns>S_OK, if the sort operation was successful; otherwise, failed.</returns>
            int SortTable(IntPtr lpSortCriteria, int ulFlags);
            /// <summary>
            /// Retrieves the current sort order for a table.
            /// </summary>
            /// <param name="lppSortCriteria">Pointer to a pointer to the SSortOrderSet structure holding the current sort order.</param>
            /// <returns>S_OK, if the current sort order was successfully returned; otherwise, failed.</returns>
            int QuerySortOrder(IntPtr lppSortCriteria);
            /// <summary>
            /// Returns one or more rows from a table, beginning at the current cursor position.
            /// </summary>
            /// <param name="lRowCount">Maximum number of rows to be returned.</param>
            /// <param name="ulFlags">Bitmask of flags that control how rows are returned.</param>
            /// <param name="lppRows">Pointer to a pointer to an SRowSet structure holding the table rows.</param>
            /// <returns>S_OK, if the rows were successfully returned; otherwise, failed.</returns>
            int QueryRows(int lRowCount, uint ulFlags, out IntPtr lppRows);
            /// <summary>
            /// Stops any asynchronous operations currently in progress for the table.
            /// </summary>
            /// <returns>S_OK, if one or more asynchronous operations have been stopped; otherwise, failed.</returns>
            int Abort();
            /// <summary>
            /// Expands a collapsed table category, adding the leaf or lower-level heading rows belonging to the category to the table view.
            /// </summary>
            /// <param name="cbInstanceKey">The count of bytes in the PR_INSTANCE_KEY property pointed to by the pbInstanceKey parameter.</param>
            /// <param name="pbInstanceKey">A pointer to the PR_INSTANCE_KEY property that identifies the heading row for the category.</param>
            /// <param name="ulRowCount">The maximum number of rows to return in the lppRows parameter. </param>
            /// <param name="ulFlags">Reserved; must be zero.</param>
            /// <param name="lppRows">A pointer to an SRowSet structure receiving the first (up to ulRowCount) rows that have been inserted into the table view as a result of the expansion.</param>
            /// <param name="lpulMoreRows">A pointer to the total number of rows that were added to the table view.</param>
            /// <returns>S_OK, if the category was expanded successfully; otherwise, failed.</returns>
            int ExpandRow(uint cbInstanceKey, IntPtr pbInstanceKey, uint ulRowCount, uint ulFlags, IntPtr lppRows, IntPtr lpulMoreRows);
            /// <summary>
            /// Collapses an expanded table category, removing any lower-level headings and leaf rows belonging to the category from the table view.
            /// </summary>
            /// <param name="cbInstanceKey">The count of bytes in the PR_INSTANCE_KEY property pointed to by the pbInstanceKey parameter.</param>
            /// <param name="pbInstanceKey">A pointer to the PR_INSTANCE_KEY property that identifies the heading row for the category. </param>
            /// <param name="ulFlags">Reserved; must be zero.</param>
            /// <param name="lpulRowCount">A pointer to the total number of rows that are being removed from the table view.</param>
            /// <returns>S_OK, if the collapse operation has succeeded; otherwise, failed.</returns>
            int CollapseRow(uint cbInstanceKey, IntPtr pbInstanceKey, uint ulFlags, IntPtr lpulRowCount);
            /// <summary>
            /// Suspends processing until one or more asynchronous operations in progress on the table have completed.
            /// </summary>
            /// <param name="ulFlags">Reserved; must be zero.</param>
            /// <param name="ulTimeout">Maximum number of milliseconds to wait for the asynchronous operation or operations to complete.</param>
            /// <param name="lpulTableStatus">On input, either a valid pointer or NULL. On output, if lpulTableStatus is a valid pointer, it points to the most recent status of the table. </param>
            /// <returns>S_OK, if the wait operation was successful; otherwise, failed.</returns>
            int WaitForCompletion(uint ulFlags, uint ulTimeout, IntPtr lpulTableStatus);
            /// <summary>
            /// Returns the data that is needed to rebuild the current collapsed or expanded state of a categorized table.
            /// </summary>
            /// <param name="ulFlags">Reserved; must be zero.</param>
            /// <param name="cbInstanceKey">The count of bytes in the instance key pointed to by the lpbInstanceKey parameter.</param>
            /// <param name="lpbInstanceKey">A pointer to the PR_INSTANCE_KEY property of the row at which the current collapsed or expanded state should be rebuilt. </param>
            /// <param name="lpcbCollapseState">A pointer to the count of structures pointed to by the lppbCollapseState parameter.</param>
            /// <param name="lppbCollapseState">A pointer to a pointer to structures that contain data that describes the current table view.</param>
            /// <returns>S_OK, if the state for the categorized table was successfully saved; otherwise, failed.</returns>
            int GetCollapseState(uint ulFlags, uint cbInstanceKey, IntPtr lpbInstanceKey, IntPtr lpcbCollapseState, IntPtr lppbCollapseState);
            /// <summary>
            /// Rebuilds the current expanded or collapsed state of a categorized table using data that was saved by a prior call to the IMAPITable::GetCollapseState method.
            /// </summary>
            /// <param name="ulFlags">Reserved; must be zero.</param>
            /// <param name="cbCollapseState">Count of bytes in the structure pointed to by the pbCollapseState parameter.</param>
            /// <param name="pbCollapseState">Pointer to the structures containing the data needed to rebuild the table view.</param>
            /// <param name="lpbkLocation">Pointer to a bookmark identifying the row in the table at which the collapsed or expanded state should be rebuilt. </param>
            /// <returns>S_OK, if the state of the categorized table was successfully rebuilt; otherwise, failed.</returns>
            int SetCollapseState(uint ulFlags, uint cbCollapseState, IntPtr pbCollapseState, IntPtr lpbkLocation);
        }

        /// <summary>
        /// The IMessage interface defines methods and properties used to manage messages.
        /// </summary>
        [ComImport()]
        [Guid(Mapi.MAPIInterfaceIds.IMessage)]        
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IMessage
        {
            int GetLastError(int hResult, uint ulFlags, out IntPtr lppMAPIError);
            int SaveChanges(uint ulFlags);
            int GetProps(IntPtr lpPropTagArray, uint ulFlags, out uint lpcValues, out IntPtr lppPropArray);
            int GetPropList(uint ulFlags, out IntPtr lppPropTagArray);
            [return: MarshalAs(UnmanagedType.I4)]
            [PreserveSig]
            int OpenProperty(uint ulPropTag, ref Guid lpiid, uint ulInterfaceOptions, uint ulFlags, out IntPtr lppUnk);
            int SetProps(uint cValues, IntPtr lpPropArray, out IntPtr lppProblems);
            int DeleteProps(IntPtr lpPropTagArray, out IntPtr lppProblems);
            int CopyTo(uint ciidExclude, ref Guid rgiidExclude, IntPtr lpExcludeProps, uint ulUIParam, IntPtr lpProgress, ref Guid lpInterface, IntPtr lpDestObj, uint ulFlags, out IntPtr lppProblems);
            int CopyProps(IntPtr lpIncludeProps, uint ulUIParam, IntPtr lpProgress, ref Guid lpInterface, IntPtr lpDestObj, uint ulFlags, out IntPtr lppProblems);
            int GetNamesFromIDs(out IntPtr lppPropTags, ref Guid lpPropSetGuid, uint ulFlags, out uint lpcPropNames, out IntPtr lpppPropNames);
            int GetIDsFromNames(uint cPropNames, ref IntPtr lppPropNames, uint ulFlags, out IntPtr lppPropTags);
            int GetAttachmentTable(uint ulFlags, out IMAPITable lppTable);
            int OpenAttach(uint ulAttachmentNum, ref Guid lpInterface, uint ulFlags, out IAttach lppAttach);
            int CreateAttach();
            int DeleteAttach();
            int GetRecipientTable(uint ulFlags, out IMAPITable lppTable);
            int ModifyRecipients();
            int SubmitMessage(uint ulFlags);
            int SetReadFlag(uint ulFlags);
        }

        [ComImport()]
        [Guid(Mapi.MAPIInterfaceIds.IAttachment)]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        public interface IAttach : IMAPIProp
        { }

        #endregion

        #region Structures

        /// <summary>
        /// The SPropValue structure describes a MAPI property.
        /// </summary>
        public struct SPropValue
        {
            /// <summary>
            /// Property tag for the property. Property tags are 32-bit unsigned integers consisting of the property's unique identifier in the high-order 16 bits and the property's type in the low-order 16 bits.
            /// </summary>
            public uint PropTag;

            /// <summary>
            /// Reserved for MAPI; do not use.
            /// </summary>
#pragma warning disable 649
            public uint DwAlignPad;
#pragma warning restore 649

            /// <summary>
            /// Union of data values, the specific value dictated by the property type.
            /// </summary>
            public PV Value;
        }

        /// <summary>
        /// Union of data values for the SPropValue.value property.
        /// </summary>
        [StructLayout(LayoutKind.Explicit, Size = 8)]
        public struct PV
        {
            [FieldOffset(0)]
            public short i;
            [FieldOffset(0)]
            public int l;
            [FieldOffset(0)]
            public uint ul;
            [FieldOffset(0)]
            public float flt;
            [FieldOffset(0)]
            public double dbl;
            [FieldOffset(0)]
            public ushort b;
            [FieldOffset(0)]
            public double at;
            [FieldOffset(0)]
            public IntPtr lpszA;
            [FieldOffset(0)]
            public IntPtr lpszW;
            [FieldOffset(0)]
            public IntPtr lpguid;
            /*[FieldOffset(0)]
            public IntPtr bin;*/
            [FieldOffset(0)]
            public ulong li;
            [FieldOffset(0)]
            public SRowSet bin;
        }

        /// <summary>
        /// Describes a row from a table that contains selected properties for a specific object.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct SRow
        {
            public uint ulAdrEntryPad;
            public uint cValues;
            public IntPtr lpProps;
        }

        /// <summary>
        /// Contains an array of SRow structures. Each SRow structure describes a row from a table.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        public struct SRowSet
        {
            public uint cRows;
            public IntPtr aRow; // pSRow

            public byte[] AsBytes
            {
                get
                {
                    byte[] b = new byte[this.cRows];
                    for (int i = 0; i < this.cRows; i++)
                        b[i] = Marshal.ReadByte(aRow, i);
                    return b;
                }
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Retrieves a managed object from the SPropValue structure that is returned by GetProps etc.
        /// </summary>
        /// <param name="mapiProperty">The MAPI property the value was retrieved for.</param>
        /// <param name="sPropValue">The value to convert.</param>
        /// <returns>The converted object or null if an error occured.</returns>
        public static object ConvertSPropValueToObject(MapiProperty.MapiProp mapiProperty, SPropValue sPropValue)
        {
            object propertyValue = null;

            try
            {
                switch (mapiProperty.DataType)
                {
                    case MapiProperty.MapiDataType.PtypBoolean:
                        {
                            propertyValue = Convert.ToBoolean(sPropValue.Value.b);
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypString:
                        {
                            if ((((uint)sPropValue.Value.lpszW).ToString("X2") is string hexValue) &&
                                (hexValue.StartsWith("800")))
                            {
                                Log.Error("ConvertSPropValueToObject: Cannot convert MAPI property {0} to string: {1}", mapiProperty.DaslName, hexValue);
                            }
                            else
                            {
                                propertyValue = Marshal.PtrToStringUni(sPropValue.Value.lpszW);
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypString8:
                        {
                            if ((((uint)sPropValue.Value.lpszA).ToString("X2") is string hexValue) &&
                                (hexValue.StartsWith("800")))
                            {
                                Log.Error("ConvertSPropValueToObject: Cannot convert MAPI property {0} to string8: {1}", mapiProperty.DaslName, hexValue);
                            }
                            else
                            {
                                propertyValue = Marshal.PtrToStringUni(sPropValue.Value.lpszA);
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypInteger16:
                        {
                            propertyValue = sPropValue.Value.i;
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypInteger32:
                        {
                            propertyValue = sPropValue.Value.l;
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypFloating32:
                        {
                            propertyValue = sPropValue.Value.flt;
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypFloating64:
                    case MapiProperty.MapiDataType.PtypFloatingTime:
                        {
                            propertyValue = sPropValue.Value.at;
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypInteger64:
                        {
                            propertyValue = sPropValue.Value.li;
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypGuid:
                        {
                            propertyValue = Marshal.PtrToStructure(sPropValue.Value.lpguid, typeof(Guid));
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypBinary:
                        {
                            propertyValue = sPropValue.Value.bin;
                        }
                        break;
                    default:
                        {
                            propertyValue = null;
                            Log.Error("ConvertSPropValueToObject: Error converting SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), mapiProperty.DataType));
                        }
                        break;
                }
            }
            catch (Exception ex)
            {
                propertyValue = null;
                Log.Error("ConvertSPropValueToObject: Error converting MAPI property {0}. Exception: {1}.", mapiProperty.DaslName, ex.ToString());
            }

            return propertyValue;
        }

        /// <summary>
        /// Gets an attachment's properties.
        /// </summary>
        /// <param name="omi">The Outlook mail item that contains the attachment.</param>
        /// <param name="index">The attachment's index. 0-based.</param>
        /// <param name="mapiProperties">The MAPI properties to get its values for.</param>
        /// <param name="propertyValues">The property values that have been returned.</param>
        /// <returns>The status (Mapi.HResult) of this method.</returns>
        public static int GetAttachmentProperties(Outlook.MailItem omi,
                                                  int index,
                                                  List<MapiProperty.MapiProp> mapiProperties,
                                                  out MAPIProperties propertyValues)
        {
            int result = (int)Mapi.HResult.S_FALSE;
            propertyValues = null;

            try
            {
                // Get the IMessage interface
                IMessage iMessage = (IMessage)omi?.MAPIOBJECT;
                if (iMessage == null)
                {
                    throw new Exception("Could not get IMessage interface.");
                }

                // Open the attachment
                Guid guid = (typeof(Mapi.IAttach)).GUID;
                result = iMessage.OpenAttach((uint)index, ref guid, 0, out IAttach attachment);

                // If we can't open the attachment, throw error
                if (result != 0)
                {
                    throw new Exception("Error opening attachment. " + Mapi.GetHResultError(result));
                }

                // Create the SPropValue structure
                Mapi.SPropValue sPropValue = new Mapi.SPropValue();
                int propValueSize = Marshal.SizeOf(sPropValue);

                // Allocate memory for the array of SPropValues to set
                int propertiesCount = mapiProperties.Count;
                IntPtr propTagArray = Marshal.AllocHGlobal(propValueSize * propertiesCount);

                // Create the property values array
                uint[] propertyTags = new uint[propertiesCount + 1];
                propertyTags[0] = (uint)propertiesCount;
                for (int i = 0; i < propertiesCount; i++)
                {
                    propertyTags[i + 1] = (uint)mapiProperties[i].Tag;
                }

                // Get properties
                result = attachment.GetProps(propertyTags, Mapi.MAPI_UNICODE, out uint valuesCount, out IntPtr propArray);

                // If an error occured, just log at this point.
                if (result != 0)
                {
                    Log.Error("OpenAttachment: Error getting attachment properties. " + Mapi.GetHResultError(result));
                }

                // Convert the retrieved values
                object[] values = new object[valuesCount];
                for (int i = 0; i < valuesCount; i++)
                {
                    sPropValue = (SPropValue)Marshal.PtrToStructure((propArray + (i * propValueSize)), typeof(SPropValue));
                    values[i] = Mapi.ConvertSPropValueToObject(mapiProperties[i], sPropValue);
                }

                // Check if returned values match properties count
                if (propertiesCount != valuesCount)
                {
                    throw new Exception("Properties count doesn't match values count.");
                }

                // Create return dictionary
                propertyValues = new MAPIProperties();
                for (int i = 0; i < valuesCount; i++)
                {
                    propertyValues.Add(mapiProperties[i], values[i]);
                }
            }
            catch (Exception ex)
            {
                Log.Error("OpenAttachment: Error getting attachment.  " + ex.ToString());
            }

            return result;
        }

        /// <summary>
        /// Gets the attachment table of an Outlook mail item.
        /// </summary>
        /// <param name="omi">The Outlook mail item to get its attachment table for.</param>
        /// <param name="attachmentTable">The attachment table.</param>
        /// <returns>The status of this method.</returns>
        public static int GetAttachmentTable(Outlook.MailItem omi, out IMAPITable attachmentTable)
        {
            int result = (int)Mapi.HResult.S_FALSE;
            attachmentTable = null;

            // Get MAPI object from mail item
            object mapiObject = omi?.MAPIOBJECT;
            if (mapiObject == null)
            {
                Log.Error("GetMAPIProperties: MAPI object is null. Property could not be set.");
                return result;
            }

            // Pointer to IUnknown interface
            IntPtr IUnknown = IntPtr.Zero;

            try
            {
                // Initialize MAPI
                NativeMethods.MAPIInitialize(IntPtr.Zero);

                // Get the IUnknown interface from the MAPI object
                IUnknown = Marshal.GetIUnknownForObject(mapiObject);

                // Get the attachment table
                IMessage message = (IMessage)mapiObject;
                result = message.GetAttachmentTable(0, out attachmentTable);
            }
            catch (Exception ex)
            {
                Log.Error("OpenMAPIProperty: Error occured. " + ex.ToString());
            }
            finally
            {
                if (IUnknown != IntPtr.Zero)
                {
                    Marshal.Release(IUnknown);
                }

                NativeMethods.MAPIUninitialize();
            }

            return result;
        }

        /// <summary>
        /// Converts an error code into a meaningful error message (if available).
        /// </summary>
        /// <param name="error">The error code to convert.</param>
        /// <returns>A string with a meaningful error code or null if an error occurs.</returns>
        public static string GetHResultError(int error)
        {
            try
            {
                foreach (var hResult in Enum.GetValues(typeof(Mapi.HResult)))
                {
                    if (((int)(uint)hResult) == error)
                    {
                        return Enum.GetName(typeof(Mapi.HResult), hResult);
                    }
                }

                // As backup, return a hex value string
                return error.ToString("X2");
            }
            catch (Exception ex)
            {
                Log.Error("GetHResultError: Error getting HResult error. " + ex.ToString());
            }

            return null;
        }

        /// <summary>
        /// Gets a MAPI property value from an Outlook mail item.
        /// </summary>
        /// <param name="omi">The Outlook mail item to get the property value for.</param>
        /// <param name="mapiProperty">The MAPI property to get its value.</param>
        /// <returns>The value or null if it doesn't exist or an error occured.</returns>
        public static object GetMAPIProperty(Outlook.MailItem omi, MapiProperty.MapiProp mapiProperty)
        {
            return Mapi.GetMAPIProperty(omi?.MAPIOBJECT, Mapi.MAPIInterfaceIds.IMessage, mapiProperty);
        }

        /// <summary>
        /// Gets a MAPI property value from an Outlook folder item.
        /// </summary>
        /// <param name="omi">The Outlook folder item to get the property value for.</param>
        /// <param name="mapiProperty">The MAPI property to get its value.</param>
        /// <returns>The value or null if it doesn't exist or an error occured.</returns>
        public static object GetMAPIProperty(Outlook.Folder folder, MapiProperty.MapiProp mapiProperty)
        {
            return Mapi.GetMAPIProperty(folder?.MAPIOBJECT, Mapi.MAPIInterfaceIds.IMAPIFolder, mapiProperty);
        }

        /// <summary>
        /// Gets the specified MAPI property value for this store.
        /// </summary>
        /// <param name="store">The Outlook store to process with.</param>
        /// <param name="mapiProperty">The MAPI property to get its value for.</param>
        /// <returns>The property value or null if not found or an error occured.</returns>
        public static object GetMAPIProperty(Outlook.Store store, MapiProperty.MapiProp mapiProperty)
        {
            return Mapi.GetMAPIProperty(store?.MAPIOBJECT, Mapi.MAPIInterfaceIds.IMAPIMsgStore, mapiProperty);
        }

        /// <summary>
        /// Gets a MAPI property value from an Outlook item.
        /// </summary>
        /// <param name="mapiObject">The MAPIOBJECT property of the Outlook item to get the property value for.</param>
        /// <param name="mapiInterfaceId">The MAPI interface id. Has to be one of the ones defined in Mapi.MAPIInterfaceIds and
        /// has to match the type of the mapiObject parameter.</param>
        /// <param name="mapiProperty">The MAPI property to get its value.</param>
        /// <returns>The value or null if it doesn't exist or an error occured.</returns>
        private static object GetMAPIProperty(object mapiObject, string mapiInterfaceId, MapiProperty.MapiProp mapiProperty)
        {
            // Check passed parameters
            if ((mapiObject == null) ||
                (string.IsNullOrEmpty(mapiInterfaceId)))
            {
                Log.Error("GetMAPIProperty: MAPI object oder interface id is null. Returning null.");
                return null;
            }

            object propertyValue = null;

            // Pointer to IUnknown interface
            IntPtr IUnknown = IntPtr.Zero;

            // Pointer to IMessage, IMAPIFolder etc. interface
            IntPtr IMAPIObject = IntPtr.Zero;

            // Pointer to IMAPIProp interface
            IntPtr IMAPIProp = IntPtr.Zero;

            // Structure that will hold the property value
            Mapi.SPropValue sPropValue;

            // A pointer that points to the SPropValue structure 
            IntPtr ptrPropValue = IntPtr.Zero;

            try
            {
                // Initialize MAPI
                NativeMethods.MAPIInitialize(IntPtr.Zero);

                // Get the IUnknown interface from the MAPI object
                IUnknown = Marshal.GetIUnknownForObject(mapiObject);

                // Set the MAPI object interface GUID that we pass to retrieve the IMessage interface.
                Guid guidIMessage = new Guid(mapiInterfaceId);

                // Try to retrieve the MAPI object interface
                if (Marshal.QueryInterface(IUnknown, ref guidIMessage, out IMAPIObject) != (uint)Mapi.HResult.S_OK)
                {
                    Log.Error("GetMAPIProperty: Could not retrieve IMessage interface. Returning null.");
                    return null;
                }

                // Set the IMAPIProp interface GUID that we pass to retrieve the IMAPIProp Interface.
                Guid guidIMAPIProp = new Guid(Mapi.MAPIInterfaceIds.IMAPIProp);

                // Try to retrieve the IMAPIProp interface
                if ((Marshal.QueryInterface(IMAPIObject, ref guidIMAPIProp, out IMAPIProp) != (uint)Mapi.HResult.S_OK) ||
                    (IMAPIProp == IntPtr.Zero))
                {
                    Log.Error("GetMAPIProperty: Could not retrieve IMAPIProp interface. Returning null.");
                    return null;
                }

                // Try to get the property
                NativeMethods.HrGetOneProp(IMAPIProp, mapiProperty.Tag, out ptrPropValue);

                if (ptrPropValue == IntPtr.Zero)
                {
                    Log.Error("GetMAPIProperty: Could not retrieve pointer to property value. Returning null.");
                    return null;
                }

                // Get the SPropValue structure
                sPropValue = (SPropValue)Marshal.PtrToStructure(ptrPropValue, typeof(SPropValue));

                // Convert the retrieved value
                propertyValue = Mapi.ConvertSPropValueToObject(mapiProperty, sPropValue);
            }
            catch (Exception ex)
            {
                propertyValue = null;
                Log.Error("GetMAPIProperty: Error occured. " + ex.ToString());
            }
            finally
            {
                // Free used memory structures
                if (ptrPropValue != IntPtr.Zero)
                {
                    NativeMethods.MAPIFreeBuffer(ptrPropValue);
                }

                // Clean up all references to COM Objects
                if (IMAPIProp != IntPtr.Zero)
                {
                    Marshal.Release(IMAPIProp);
                }

                if (IMAPIObject != IntPtr.Zero)
                {
                    Marshal.Release(IMAPIObject);
                }

                if (IUnknown != IntPtr.Zero)
                {
                    Marshal.Release(IUnknown);
                }

                NativeMethods.MAPIUninitialize();
            }

            return propertyValue;
        }

        /// <summary>
        /// Gets a set of MAPI property values from an Outlook item.
        /// </summary>
        /// <param name="mapiObject">The MAPIOBJECT property of the Outlook item to get the property values for.</param>
        /// <param name="mapiProperties">The MAPI properties to get their values for.</param>
        /// <param name="propertyValues">The retrieved values.</param>
        /// <returns>The error code returned by IMAPIProp::GetProps or 0 if no error occured.</returns>
        internal static int GetMAPIProperties(object mapiObject, List<MapiProperty.MapiProp> mapiProperties, out MAPIProperties propertyValues)
        {
            int result = (int)Mapi.HResult.S_FALSE;
            propertyValues = null;

            // Get MAPI object from mail item
            if (mapiObject == null)
            {
                Log.Error("GetMAPIProperties: MAPI object is null. Property could not be set.");
                return result;
            }

            // Check if we actually have some properties
            int propertiesCount = mapiProperties.Count;
            if (propertiesCount < 1)
            {
                Log.Error("GetMAPIProperties: No properties found.");
                return result;
            }

            // Pointer to IUnknown interface
            IntPtr IUnknown = IntPtr.Zero;

            // Pointer to the MAPI properties array
            IntPtr propTagArray = IntPtr.Zero;

            // Pointer to the value
            IntPtr valuePtr = IntPtr.Zero;

            try
            {
                // Initialize MAPI
                NativeMethods.MAPIInitialize(IntPtr.Zero);

                // Get the IUnknown interface from the MAPI object
                IUnknown = Marshal.GetIUnknownForObject(mapiObject);

                // Get the IMAPIProp interface
                Mapi.IMAPIProp IMAPIProp = (Mapi.IMAPIProp)Marshal.GetTypedObjectForIUnknown(IUnknown, typeof(Mapi.IMAPIProp));

                // Create the SPropValue structure
                Mapi.SPropValue sPropValue = new Mapi.SPropValue();
                int propValueSize = Marshal.SizeOf(sPropValue);

                // Allocate memory for the array of SPropValues to set
                propTagArray = Marshal.AllocHGlobal(propValueSize * propertiesCount);

                // Create the property values array
                uint[] propertyTags = new uint[propertiesCount + 1];
                propertyTags[0] = (uint)propertiesCount;
                for (int i = 0; i < propertiesCount; i++)
                {
                    propertyTags[i + 1] = (uint)mapiProperties[i].Tag;
                }

                // Get properties
                result = IMAPIProp.GetProps(propertyTags, Mapi.MAPI_UNICODE, out uint valuesCount, out IntPtr propArray);

                // Convert the retrieved values
                object[] values = new object[valuesCount];
                for (int i = 0; i < valuesCount; i++)
                {
                    sPropValue = (SPropValue)Marshal.PtrToStructure((propArray + (i * propValueSize)), typeof(SPropValue));
                    values[i] = Mapi.ConvertSPropValueToObject(mapiProperties[i], sPropValue);
                }

                // Check if returned values match properties count
                if (propertiesCount != valuesCount)
                {
                    Log.Error("GetMAPIProperties: Properties count doesn't match values count.");
                    return result;
                }

                // Create return dictionary
                propertyValues = new MAPIProperties();
                for (int i = 0; i < valuesCount; i++)
                {
                    propertyValues.Add(mapiProperties[i], values[i]);
                }
            }
            catch (Exception ex)
            {
                Log.Error("SetMAPIProperties: Error occured. " + ex.ToString());
            }
            finally
            {
                if (IUnknown != IntPtr.Zero)
                {
                    Marshal.Release(IUnknown);
                }

                if (propTagArray != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(propTagArray);
                }

                if (valuePtr != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(valuePtr);
                }

                NativeMethods.MAPIUninitialize();
            }

            return result;
        }

        /// <summary>
        /// Gets a list of all table rows and their MAPI properties using the IMAPITable.QueryRows method.
        /// Each list entry in the return value corresponds to a table row, while each list entry's dictionary
        /// is a set of property value pairs returned for the table row.
        /// This code is based on Fred Song's Managed MAPI: 
        /// https://www.codeproject.com/Articles/455823/Managed-MAPI-Part-1-Logon-MAPI-Session-and-Retriev
        /// </summary>
        /// <param name="mapiTable">The table to query its rows.</param>
        /// <param name="rowCount">The maximum count of rows to query.</param>
        /// <param name="values">The returned values.</param>
        /// <returns>The status code of the IMAPITable.QueryRows method.</returns>
        public static int GetTableProperties(Mapi.IMAPITable mapiTable, int rowCount, out List<Dictionary<uint, object>> properties)
        {
            IntPtr pRowSet = IntPtr.Zero;
            properties = null;

            int status = mapiTable.QueryRows(rowCount, 0, out pRowSet);
            if (status != (uint)Mapi.HResult.S_OK)
            {
                NativeMethods.MAPIFreeBuffer(pRowSet);
            }

            uint cRows = (uint)Marshal.ReadInt32(pRowSet);
            if (cRows < 1)
            {
                NativeMethods.MAPIFreeBuffer(pRowSet);
                return status;
            }

            int pIntSize = IntPtr.Size, intSize = Marshal.SizeOf(typeof(Int32));
            int sizeOfSRow = 2 * intSize + pIntSize;
            IntPtr rows = pRowSet + intSize;
            properties = new List<Dictionary<uint, object>>();
            for (int i = 0; i < cRows; i++)
            {
                IntPtr pRowOffset = rows + i * sizeOfSRow;
                uint cValues = (uint)Marshal.ReadInt32(pRowOffset + pIntSize);
                IntPtr pProps = Marshal.ReadIntPtr(pRowOffset + pIntSize + intSize);
                
                int size =  Marshal.SizeOf(typeof(SPropValue));

                Dictionary<uint, object> propValues = new Dictionary<uint, object>();
                for (int j = 0; j < cValues; j++) // each column
                {
                    SPropValue lpProp = (SPropValue)Marshal.PtrToStructure((pProps + (j * size)), typeof(SPropValue));
                    propValues.Add(lpProp.PropTag, lpProp.Value);
                }

                properties.Add(propValues);
            }

            NativeMethods.MAPIFreeBuffer(pRowSet);
            return status;
        }

        /// <summary>
        /// Sets the given MAPI properties on the Outlook mail item.
        /// Note: the caller has to make sure that the count of MAPI properties
        /// and respective values matches and that the given values match the
        /// required data type for each MAPI property.
        /// Do not call this method from a background thread!
        /// </summary>
        /// <param name="omi">The Outlook mail item to set its properties.</param>
        /// <param name="mapiProperties">The MAPI properties to set.</param>
        /// <param name="values">The values to set.</param>
        /// <returns>True if the method succeeds, otherwise false.</returns>
        public static bool SetMAPIProperties(Outlook.MailItem omi, MapiProperty.MapiProp[] mapiProperties, object[] values)
        {
            // The return status of this method
            bool success = false;

            // Pointer to IUnknown interface
            IntPtr IUnknown = IntPtr.Zero;

            // Pointer to the MAPI properties array
            IntPtr propArray = IntPtr.Zero;

            // Pointer to the value
            IntPtr valuePtr = IntPtr.Zero;

            // Get MAPI object from mail item
            object mapiObject = omi?.MAPIOBJECT;
            if (mapiObject == null)
            {
                Log.Error("SetMAPIProperties: MAPI object is null. Property could not be set.");
                return success;
            }

            // Make sure the properties count matches the values count
            int propCount = mapiProperties.Length;
            if (propCount != values.Length)
            {
                Log.Error("SetMAPIProperties: Mismatch between tag count and value count.");
                return success;
            }

            try
            {
                // Initialize MAPI
                NativeMethods.MAPIInitialize(IntPtr.Zero);

                // Get the IUnknown interface from the MAPI object
                IUnknown = Marshal.GetIUnknownForObject(mapiObject);

                // Get the IMAPIProp interface
                Mapi.IMAPIProp IMAPIProp = (Mapi.IMAPIProp)Marshal.GetTypedObjectForIUnknown(IUnknown, typeof(Mapi.IMAPIProp));

                // Create the SPropValue structure
                Mapi.SPropValue sPropValue = new Mapi.SPropValue();
                var propValueSize = Marshal.SizeOf(sPropValue);

                // Allocate memory for the array of SPropValues to set
                propArray = Marshal.AllocHGlobal(propValueSize * propCount);

                // Create the property values array
                for (int i = 0; i < propCount; i++)
                {
                    // Get the current property/value pair
                    MapiProperty.MapiProp mapiProperty = mapiProperties[i];
                    uint tag = mapiProperty.Tag;
                    object value = values[i];

                    // Set the property value's property tag
                    sPropValue.PropTag = (uint)tag;

                    // Set the property value's value
                    switch (mapiProperty.DataType)
                    {
                        case MapiProperty.MapiDataType.PtypBoolean:
                            {
                                try
                                {
                                    sPropValue.Value.b = (ushort)Convert.ToInt16((bool)value);
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Boolean. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypString:
                            {
                                try
                                {
                                    valuePtr = Marshal.StringToHGlobalUni(value as string);
                                    sPropValue.Value.lpszW = valuePtr;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to String. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypString8:
                            {
                                try
                                {
                                    valuePtr = Marshal.StringToHGlobalAnsi(value as string);
                                    sPropValue.Value.lpszA = valuePtr;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to String8. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypInteger16:
                            {
                                try
                                {
                                    sPropValue.Value.i = (short)value;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Short. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypInteger32:
                            {
                                try
                                {
                                    sPropValue.Value.l = (int)value;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Integer. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypFloating32:
                            {
                                try
                                {
                                    sPropValue.Value.flt = (float)value;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Float. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypFloating64:
                        case MapiProperty.MapiDataType.PtypFloatingTime:
                            {
                                try
                                {
                                    sPropValue.Value.at = (double)value;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Double. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypInteger64:
                            {
                                try
                                {
                                    sPropValue.Value.li = (ulong)value;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Ulong. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypGuid:
                            {
                                try
                                {
                                    IntPtr guidPtr = Marshal.AllocHGlobal(Marshal.SizeOf(value));
                                    Marshal.StructureToPtr((Guid)value, guidPtr, false);
                                    sPropValue.Value.lpguid = guidPtr;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Guid. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        case MapiProperty.MapiDataType.PtypBinary:
                            {
                                try
                                {
                                    sPropValue.Value.bin = (Mapi.SRowSet)value;
                                }
                                catch (Exception ex)
                                {
                                    throw new Exception(string.Format("Error converting to Binary. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                                }
                            }
                            break;
                        default:
                            {
                                throw new Exception(string.Format("Error creating SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), mapiProperty.DataType)));
                            }
                    }

                    Marshal.StructureToPtr(sPropValue, (IntPtr)((uint)propArray + (propValueSize * i)), false);

                    if (valuePtr != IntPtr.Zero)
                    {
                        Marshal.FreeHGlobal(valuePtr);
                    }
                }

                // Set properties and save
                IntPtr problems = IntPtr.Zero;
                if (IMAPIProp.SetProps((uint)propCount, propArray, problems) == (uint)Mapi.HResult.S_OK)
                {
                    success = (IMAPIProp.SaveChanges((uint)Mapi.SaveOption.KEEP_OPEN_READWRITE) == (uint)Mapi.HResult.S_OK);
                }
            }
            catch (Exception ex)
            {
                Log.Error("SetMAPIProperties: Error occured. " + ex.ToString());
                success = false;
            }
            finally
            {
                if (IUnknown != IntPtr.Zero)
                {
                    Marshal.Release(IUnknown);
                }

                if (propArray != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(propArray);
                }

                if (valuePtr != IntPtr.Zero)
                {
                    Marshal.FreeHGlobal(valuePtr);
                }

                NativeMethods.MAPIUninitialize();
            }

            return success;
        }

        /// <summary>
        /// Sets the given MAPI property on the Outlook mail item.
        /// Note: the caller has to make sure that the given value matches the
        /// required data type for the MAPI property.
        /// Do not call this method from a background thread!
        /// </summary>
        /// <param name="omi">The Outlook mail item to set the property for.</param>
        /// <param name="mapiProperties">The MAPI property to set.</param>
        /// <param name="values">The value to set.</param>
        /// <param name="useSetProps">Whether to use the IMAPIProp.SetProps method (instead of HrSetOneProp).</param>
        /// <returns>True if the method succeeds, otherwise false.</returns>
        public static bool SetMAPIProperty(Outlook.MailItem omi,
                                           MapiProperty.MapiProp mapiProperty,
                                           object value,
                                           bool useSetProps = false)
        {
            if (useSetProps)
            {
                return Mapi.SetMAPIProperties(omi, new MapiProperty.MapiProp[] { mapiProperty }, new object[] { value });
            }

            // The return status of this method
            bool success = false;

            // Pointer to IUnknown interface
            IntPtr IUnknown = IntPtr.Zero;

            // Pointer to IMessage interface
            IntPtr IMessage = IntPtr.Zero;

            // Pointer to IMAPIProp interface
            IntPtr IMAPIProp = IntPtr.Zero;

            // Structure that will hold the property value
            Mapi.SPropValue sPropValue;

            // A pointer that points to the SPropValue structure 
            IntPtr ptrPropValue = IntPtr.Zero;

            // Pointer to the value
            IntPtr valuePtr = IntPtr.Zero;

            // Get MAPI object from mail item
            object mapiObject = omi?.MAPIOBJECT;
            if (mapiObject == null)
            {
                Log.Error("SetMAPIProperty: MAPI object is null. Property could not be set.");
                return success;
            }

            try
            {
                // Initialize MAPI
                NativeMethods.MAPIInitialize(IntPtr.Zero);

                // Get the IUnknown interface from the MAPI object
                IUnknown = Marshal.GetIUnknownForObject(mapiObject);

                // Set the IMessage interface GUID that we pass to retrieve the IMessage interface.
                Guid guidIMessage = new Guid(Mapi.MAPIInterfaceIds.IMessage);

                // Try to retrieve the IMessage interface
                if (Marshal.QueryInterface(IUnknown, ref guidIMessage, out IMessage) != (uint)Mapi.HResult.S_OK)
                {
                    Log.Error("SetMAPIProperty: Could not retrieve IMessage interface. Property could not be set.");
                    return success;
                }

                // Set the IMAPIProp interface GUID that we pass to retrieve the IMAPIProp Interface.
                Guid guidIMAPIProp = new Guid(Mapi.MAPIInterfaceIds.IMAPIProp);

                // Try to retrieve the IMAPIProp interface
                if ((Marshal.QueryInterface(IMessage, ref guidIMAPIProp, out IMAPIProp) != (uint)Mapi.HResult.S_OK) ||
                    (IMAPIProp == IntPtr.Zero))
                {
                    Log.Error("SetMAPIProperty: Could not retrieve IMAPIProp interface. Property could not be set.");
                    return success;
                }

                // Create the SPropValue structure
                sPropValue = new Mapi.SPropValue
                {
                    PropTag = (uint)mapiProperty.Tag
                };

                // Set the property value's value
                switch (mapiProperty.DataType)
                {
                    case MapiProperty.MapiDataType.PtypBoolean:
                        {
                            try
                            {
                                sPropValue.Value.b = (ushort)Convert.ToInt16((bool)value);
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Boolean. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypString:
                        {
                            try
                            {
                                valuePtr = Marshal.StringToHGlobalUni(value as string);
                                sPropValue.Value.lpszW = valuePtr;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to String. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypString8:
                        {
                            try
                            {
                                valuePtr = Marshal.StringToHGlobalAnsi(value as string);
                                sPropValue.Value.lpszA = valuePtr;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to String8. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypInteger16:
                        {
                            try
                            {
                                sPropValue.Value.i = (short)value;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Short. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypInteger32:
                        {
                            try
                            {
                                sPropValue.Value.l = (int)value;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Integer. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypFloating32:
                        {
                            try
                            {
                                sPropValue.Value.flt = (float)value;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Float. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypFloating64:
                    case MapiProperty.MapiDataType.PtypFloatingTime:
                        {
                            try
                            {
                                sPropValue.Value.at = (double)value;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Double. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypInteger64:
                        {
                            try
                            {
                                sPropValue.Value.li = (ulong)value;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Ulong. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypGuid:
                        {
                            try
                            {
                                IntPtr guidPtr = Marshal.AllocHGlobal(Marshal.SizeOf(value));
                                Marshal.StructureToPtr((Guid)value, guidPtr, false);
                                sPropValue.Value.lpguid = guidPtr;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Guid. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    case MapiProperty.MapiDataType.PtypBinary:
                        {
                            try
                            {
                                sPropValue.Value.bin = (Mapi.SRowSet)value;
                            }
                            catch (Exception ex)
                            {
                                throw new Exception(string.Format("Error converting to Binary. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
                            }
                        }
                        break;
                    default:
                        {
                            throw new Exception(string.Format("Error creating SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), mapiProperty.DataType)));
                        }
                }

                // Get pointer to property value structure
                ptrPropValue = Marshal.AllocHGlobal(Marshal.SizeOf(sPropValue));
                Marshal.StructureToPtr(sPropValue, ptrPropValue, false);

                // Try to set the property
                NativeMethods.HrSetOneProp(IMAPIProp, ptrPropValue);

                // Save changes
                Mapi.IMAPIProp mapiProp = (Mapi.IMAPIProp)Marshal.GetTypedObjectForIUnknown(IUnknown, typeof(Mapi.IMAPIProp));
                success = (mapiProp.SaveChanges((uint)Mapi.SaveOption.KEEP_OPEN_READWRITE) == (uint)Mapi.HResult.S_OK);
            }
            catch (Exception ex)
            {
                Log.Error("SetMAPIProperty: Error occured. " + ex.ToString());
                success = false;
            }
            finally
            {
                // Free used memory structures
                if (ptrPropValue != IntPtr.Zero)
                {
                    NativeMethods.MAPIFreeBuffer(ptrPropValue);
                }

                // Clean up all references to COM Objects
                if (IMAPIProp != IntPtr.Zero)
                {
                    Marshal.Release(IMAPIProp);
                }

                if (IMessage != IntPtr.Zero)
                {
                    Marshal.Release(IMessage);
                }

                if (IUnknown != IntPtr.Zero)
                {
                    Marshal.Release(IUnknown);
                }

                NativeMethods.MAPIUninitialize();
            }

            return success;
        }

        /// <summary>
        /// Calls SaveChanges() with the given flag.
        /// </summary>
        /// <param name="omi">The Outlook mail item to save.</param>
        /// <param name="saveOption">The save flag to pass.</param>
        /// <returns>The status of this method.</returns>
        public static int Save(Outlook.MailItem omi, Mapi.SaveOption saveOption)
        {
            int status = (int)Mapi.HResult.S_FALSE;

            // Pointer to IUnknown interface
            IntPtr IUnknown = IntPtr.Zero;

            // A pointer that points to the SPropValue structure 
            IntPtr ptrPropValue = IntPtr.Zero;

            // Get MAPI object from mail item
            object mapiObject = omi?.MAPIOBJECT;
            if (mapiObject == null)
            {
                Log.Error("SetMAPIProperty: MAPI object is null. Property could not be set.");
                return status;
            }

            try
            {
                // Initialize MAPI
                NativeMethods.MAPIInitialize(IntPtr.Zero);

                // Get the IUnknown interface from the MAPI object
                IUnknown = Marshal.GetIUnknownForObject(mapiObject);

                // Save changes
                Mapi.IMAPIProp mapiProp = (Mapi.IMAPIProp)Marshal.GetTypedObjectForIUnknown(IUnknown, typeof(Mapi.IMAPIProp));
                status = mapiProp.SaveChanges((uint)saveOption);
            }
            catch (Exception ex)
            {
                Log.Error("SetMAPIProperty: Error occured. " + ex.ToString());
                status = (int)Mapi.HResult.S_FALSE;
            }
            finally
            {
                // Free used memory structures
                if (ptrPropValue != IntPtr.Zero)
                {
                    NativeMethods.MAPIFreeBuffer(ptrPropValue);
                }

                // Clean up all references to COM Objects
                if (IUnknown != IntPtr.Zero)
                {
                    Marshal.Release(IUnknown);
                }

                NativeMethods.MAPIUninitialize();
            }

            return status;
        }
        #endregion
    }
}