Biblioteki COM i Outlook oraz Excel w C#

Autor: Artur Protasewicz


Chciałem się podzielić fragmentami mojego programu, który mi pomaga w tworzeniu reguł Outlooka, jako obrony przed spamem w firmie. Oczywiste jest, że taka metoda to odwracanie kota ogonem. Można się zapytać, czy nie prościej utworzyć wirtualny serwer poczty w firmie i mieć z tym spokój. Otóż nie. Mój budżet na IT jest bliski zera. Jest rok 2012 i trwa kryzys, który dotyka wiele firm w Polsce i na świecie. Zastałem sytuację, w której adres firmowy zosatał podany do wiadomości publicznej w internecie w formie niezaszyfrowanej i nie wiem ile lat tam był. Chętnie bym sobie sprawił rozwiązanie sprzętowe i serwis jego dostawcy, ale to jest bardzo drogie – zakup jest drogi i abonament jest drogi. Dodatkowo w spamie często są zapytania ofertowe i trzeba ten spam po prostu wpuszczać do firmy i czytać, bo być może znajdzie się kolejne zlecenie dla zakładu.


Ponieważ temat bibliotek COM bywa mało znany, moim drugim celem i chyba ważnieszym jest pokazanie, jak to się robi w przypadku bardzo popularnych programów MS Outlook i MS Excel. Źródeł mojej wiedzy jest wiele, głównie MSDN i różne fora, jednak kiedy weźmie się te kody pod lupę i przetestuje, to często pozostaje zdać się samemu na siebie. Nie sposób zapisywać wszystkich źródeł podczas szukania i testowania. Mam nadzieję, że mój kod wystarczy, jako baza do tworzenia bardziej rozbudowanych metod walki ze spamem w sytuacjach podobnych do mojej. Jeśli chodzi o COM, to niewiele jest tu do wymyślania – robi się to tak, a nie inaczej i margines swobody jest raczej mały. Jeśli zapoznasz się z kodem, a jest dużo komentarzy, będziesz w stanie tworzyć pożyteczne programy, niekoniecznie służące do walki ze spamem.


Spam to tylko pretekst, choć trochę się zastanawiałem, czy od tej publikacji ubędzie spamu, czy przybędzie. Broń jest obosieczna – można wykorzystać do obrony, a można do ataku spamerskiego. W ostateczności, zamiast używać reguł Outlooka, rozwinę program i posłużę się nim i bazą danych. Sekretarka, która już w tej chwili masę czasu traci na eliminowanie spamu i coraz mnie się w tym rozeznaje, będzie klikała jeden przycisk i cała Skrzynka odbiorcza Outlooka zostanie wyczyszczona bez używania reguł. Ale uwaga: nie możesz po prostu spamu usuwać, bo jako informatyk nie wiesz wszystkiego o tym, na które spamerskie wiadomości jednak firma odpowiedziała. Lepiej jest przenosić do osobnego folderu, żeby w razie czego dało się coś odnaleźć.


Będę rozwijał ten program, choć nie wiem, czy będę miał czas (bynajmniej nie ze względu na spam) na kolejną publikację, a także na odpowiedzi na posty, za co z góry przepraszam. Na chwilę obecną ograniczyłem się do jednego obrazka, żeby było trochę jaśniej. Ponieważ pracuję na adresach firmowych, nie mogłem pokazać wszystkiego. Nie mogłem też pokazać adresów spamerów, żeby mnie nie zaczęli ciągać po sądach. Poniżej fragment typowy dla spamu:



„Szanując Państwa prywatność i będąc w zgodzie z polskim prawem, proszę o zgodę na przesłanie oferty naszej firmy”



I tu z reguły następuje oferta mniej lub bardziej zakamuflowana.


Aby utworzyć regułę wiadomości w Outlooku, można się również posłużyć biblioteką COM, ale jeszcze do tego nie doszedłem. Jedyne sensowne reguły, które działają w Outlooku, to te, które bazują na liście konkretnych adresów email i stąd potrzeba wyselekcjonowania tych adresów (potem oddzielenia średnikami i wklejenia do reguły), choć wstępnie pomoga mi Kaspersky, którego cały czas uczę (na szczęście na to pozwala). Dosłownie idę na ryby i łowię spam przy odrobinie systematyczności. Potem używam omawianego programu i tworzę regułę wiadomości. Przynajmniej w moim przypadku adresy i nazwy większości spamerów znam już na pamięć, co znaczy, że się powtarzają i takie rozwiązanie ma jakiś sens. Pozostawiam nieomówione takie tematy, jak podgląd komórek w gridzie, ich bardziej złożoną analizę i ostateczną selekcję adresów spamerów w Excelu (autofiltr, usuwanie wierszy).


Pozostawiam Was z jednym obrazkiem i komentarzami w kodzie. Program został przetestowany na Windows XP, Windows Vista, Windows 7, Outlook/Excel 2003, Outlook/Excel 2010 i się sprawdza, choć to wciąż wersja rozwojowa.


Chociaż trochę ułatwiłem pracę sekretarce.

SpamSelector.jpg

//Założenie: Outlook jest zainstalowany
//Założenie: Do projektu dodano referencję do biblioteki COM: Microsoft Outlook 14.0 Object Library lub starszej
//Założenie: Excel jest zainstalowany
//Założenie: Do projektu dodano referencję do biblioteki COM: Microsoft Excel 14.0 Object Library lub starszej

using System;
using System.Windows.Forms;
using System.Collections;
using System.Diagnostics;
using Outlook = Microsoft.Office.Interop.Outlook; 
using Excel = Microsoft.Office.Interop.Excel;

namespace SpamSelectorNS
{
    public partial class FormSpamSelector : Form
    {
        //Konstruktor formy - nie jest tu istotny
        
        public FormSpamSelector()
        {
            InitializeComponent();
            StartPosition = FormStartPosition.Manual;
            Location = new System.Drawing.Point(0, 0);
            Size = new System.Drawing.Size(Screen.GetWorkingArea(this).Width, Screen.GetWorkingArea(this).Height);
            Load += new EventHandler(FormSpamSelector_Load);
            FormClosing += new FormClosingEventHandler(FormSpamSelector_FormClosing);
            bRunOutlook.Click += new EventHandler(bRunOutlook_Click);
            bCreateSpamFolder.Click += new EventHandler(bCreateSpamFolder_Click);
            bReportToExcel.Click += new EventHandler(bReportToExcel_Click);
            bGetFolderExplicit.Click += new EventHandler(bGetFolderExplicit_Click);
            bGetAnyUserFolder.Click += new EventHandler(bGetAnyUserFolder_Click);
            labelItemOfAll.Text = "0/0";
        }

        //Funkcja uruchamiająca Outlooka
        //Wcześniej zabijany(-ne) jest(są) proces(-y) OUTLOOK
        //Archaiczne: kiedyś trzeba by napisać outlook.exe lub nawet <ścieżka>\\outlook.exe

        void bRunOutlook_Click(object sender, EventArgs e)
        {
            Process[] processList = Process.GetProcesses();
            foreach (Process pKill in processList)
            {
                if (pKill.ProcessName.ToLower() == "outlook")
                {
                    pKill.Kill();

                    //Wziąłem break w komentarz, bo podczas testów zdarza się namnożyć procesów i jest ich więcej niż 1
                    //i wtedy program nie działa
                    
                    //break; 
                }
            }

            //Miłe wspomnienie po ShellExecute z Delphi
            
            System.Diagnostics.Process pRun = new System.Diagnostics.Process();
            pRun.StartInfo.FileName = "outlook.exe";
            pRun.Start();
        }

        //Funkcja tworząca podfolder Skrzynki odbiorczej o nazwie SPAM COLLECT (może być inna nazwa)
        //U siebie robię to w oparciu o folder Wiadomości-śmieci, ale funkcja może się przydać

        void bCreateSpamFolder_Click(object sender, EventArgs e)
        {
            Outlook.Application app = null;
            Outlook._NameSpace ns = null;
            Outlook.MAPIFolder folder = null;

            try
            {
                app = new Outlook.Application();
                ns = app.GetNamespace("MAPI");

                //U mnie na komputerze Logon jest zbędne, ale nie wiem, czy nie będzie potrzebne w starszych wersjach
                //i czy nie będzie się pojawiało pytanie o zezwolenie na dostęp do Outlooka z zewnętrznego programu
                
                //ns.Logon(null, null, false, false); 

                folder = ns.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
                
                //Próba utworzenia folderu, który już istnieje, powoduje błąd - stąd try

                try 
                {
                    folder.Folders.Add("SPAM COLLECT");
                }
                catch { }
            }
            catch (System.Runtime.InteropServices.COMException ex)
            {
                MessageBox.Show(ex.ToString());
            }
            finally
            {
                app = null;
                ns = null;
                folder = null;
            }
        }

        //Opcje wybierania folderu zawierającego spam
        
        enum SpamFolderSelcectOption
        {
            optFolderExplicit, //Wpisany na sztywno tutaj w kodzie
            optAnyUserFolder //Wybierany przy pomocy okna dialogowego Outlooka
        };

        //Funkcja pobierająca spam do grida
        //Każdy istostny w analizie spamu element wiadomości w osobnej kolumnie
        //Uwaga: Nie wszystkie adresy email nadawców spamu będą poprawne np. może się zdarzyć artur@protasewicz i koniec
        //Jest to istotne przy tworzeniu reguł

        void GetSpam(SpamFolderSelcectOption Option)
        {
            Outlook.Application app = null;
            Outlook._NameSpace ns = null;
            Outlook.MAPIFolder spamFolder = null;
            Outlook.MailItem item = null;
            int ctrAll = 0;
            int ctrItem = 0;
            string[,] result;

            try
            {
                app = new Outlook.Application();
                ns = app.GetNamespace("MAPI");
                
                //ns.Logon(null, null, false, false);
                
                if (Option == SpamFolderSelcectOption.optFolderExplicit)
                {
                    spamFolder = ns.Folders["Foldery osobiste"].Folders["Wiadomości-śmieci"];
                }
                else if (Option == SpamFolderSelcectOption.optAnyUserFolder)
                {
                    spamFolder = ns.PickFolder();
                }

                if (spamFolder != null) //Będzie null, kiedy w PickFolder klikniesz Anuluj
                {
                    result = new string[gridSpam.ColumnCount, spamFolder.Items.Count];
                    int rowCount = 0;
                    gridSpam.Rows.Clear();
                    
                    //Licznik X z Y, żeby było widać, że coś się dzieje i ile jeszcze zostało
                    //Element Y
                    
                    ctrAll = spamFolder.Items.Count;
                    labelItemOfAll.Text = "0/0";

                    //Miłe wspomnienie po ProcessMessages z Delphi

                    Application.DoEvents();
                    
                    //Poniżej fragment dwóczęściowy, żeby było szybciej
                    //Łatwo wpaść w pułapkę dodawania elementów wizualnych wiersz po wierszu do grida

                    //(1) najpierw do tablicy
                    for (int i = 1; i <= spamFolder.Items.Count; i++)
                    {
                        rowCount++;

                        //Licznik X z Y
                        //Elementy X i Y razem, jako X/Y
                        //Odświeżanie etykiety jest znacznie tańsze czasowo, niż odświeżanie paska postępu

                        ctrItem = rowCount;
                        labelItemOfAll.Text = ctrItem.ToString() + "/" + ctrAll.ToString();

                        //Miłe wspomnienie po ProcessMessages z Delphi
                        
                        Application.DoEvents();
                        
                        //Aktualnie rozpatrywany email

                        item = (Outlook.MailItem)spamFolder.Items[i];
                        
                        //Lp

                        string sItemNumber = i.ToString();

                        //Temat emaila
                        //SPAM i Probable Spam jest u mnie dodawane przez Kasprskiego
                        //Słowo Coalesce zaczerpnięte z SQL - tu zamienia null na pusty string, jednak to już string, a nie null
                        //Jest to przydatne, jeżeli chcemy używać CellClick i oglądać zawartość
                        
                        string sSubject = Coalesce(item.Subject).Replace("[!! SPAM]", "").Replace("[?? Probable Spam]", "").Trim();
                        
                        //Adres email nadawcy
                        
                        string sSender = Coalesce(item.SenderEmailAddress);
                        
                        //Nazwa nadawcy
                        
                        string sSenderName = Coalesce(item.SenderName);
                        
                        //Adres email odbiorcy
                        
                        string sRecipient = "";
                        try
                        {
                            //Czasami w spamie nie ma żadnego odbiorcy i nie istnieje indeks 1 stąd try
                            //Sprawdzanie jednego adresata okazało się w moim przypadku wystarczające

                            sRecipient = Coalesce(item.Recipients[1].Address);
                        }
                        catch { }

                        //Treść emaila, gdyby ktoś chciał je analizować dokładniej

                        string sBody = Coalesce(item.Body);

                        //Nazwy kolumn pochodzą z mojego projektu - cItemNumber, cSubject, itd.
                        //Można tu do testów wstawić cyfry 0..5, pod warunkiem utworzenia grida z 6-cioma kolumnami

                        result[cItemNumber.Index, rowCount - 1] = sItemNumber;
                        result[cSubject.Index, rowCount - 1] = sSubject;
                        result[cSenderEmail.Index, rowCount - 1] = sSender;
                        result[cRecipient.Index, rowCount - 1] = sRecipient;
                        result[cBody.Index, rowCount - 1] = sBody;
                        result[cSenderName.Index, rowCount - 1] = sSenderName;
                    }

                    //(2) potem do grida
                    gridSpam.RowCount = spamFolder.Items.Count;
                    for (int r = 0; r < rowCount; r++)
                    {
                        gridSpam[cItemNumber.Index, r].Value = result[cItemNumber.Index, r];
                        gridSpam[cSubject.Index, r].Value = result[cSubject.Index, r];
                        gridSpam[cSenderEmail.Index, r].Value = result[cSenderEmail.Index, r];
                        gridSpam[cRecipient.Index, r].Value = result[cRecipient.Index, r];
                        gridSpam[cBody.Index, r].Value = result[cBody.Index, r];
                        gridSpam[cSenderName.Index, r].Value = result[cSenderName.Index, r];
                    }
                }
            }
            catch (System.Runtime.InteropServices.COMException ex)
            {
                MessageBox.Show(ex.ToString());
            }
            finally
            {
                app = null;
                ns = null;
                spamFolder = null;
                item = null;
            }
        }

        //Funkcja przepisująca grida do Excela i zapisująca wynik do pliku
        //Uwaga: Nie wszystkie adresy email nadawców spamu będą poprawne np. może się zdarzyć artur@protasewicz i koniec
        //Jest to istotne przy tworzeniu reguł
        //Przydaje się znajomość VBA

        void bReportToExcel_Click(object sender, EventArgs e)
        {
            Excel._Application app = null;
            Excel._Workbook workbook = null;
            Excel._Worksheet worksheet = null;
            ArrayList noDup = new ArrayList();

            try
            {
                app = new Excel.Application();
                workbook = app.Workbooks.Add(Type.Missing);
                app.Visible = true;
                worksheet = workbook.Sheets["Arkusz1"]; //W wersjach angielskojęzycznych Sheet1
                worksheet = workbook.ActiveSheet;
                worksheet.Name = "Spam";

                //Kopiowanie nagłówków grida do Excela
                //W Excelu jest Cells[<wiersz>, <kolumna>] i indeksy są od 1 a nie od 0
                //Pirwsza kolumna, żeby szef mógł zaznaczyć, czego nie blokować


                worksheet.Cells[1, 1] = "Nie blokuj";

                worksheet.Cells[1, 2] = gridSpam.Columns[cSenderName.Index].HeaderText;
                worksheet.Cells[1, 3] = gridSpam.Columns[cSubject.Index].HeaderText;
                worksheet.Cells[1, 4] = gridSpam.Columns[cSenderEmail.Index].HeaderText;

                //Kopiowanie wierszy grida do Excela z wyłączeniem duplikatów nadawców

                int j = 2;
                for (int i = 0; i < gridSpam.Rows.Count - 1; i++)
                {
                    string senderEmail = gridSpam.Rows[i].Cells[cSenderEmail.Index].FormattedValue.ToString().ToLower();
                    if (noDup.IndexOf(senderEmail) < 0) //Nie chcemy duplikatów emaili spamerów
                    {
                        noDup.Add(senderEmail);

                        //Celowo użyłem FormattedValue - to też zamienia null przynajmniej na pusty string i już nie null

                        worksheet.Cells[j, 2] = gridSpam.Rows[i].Cells[cSenderName.Index].FormattedValue.ToString();
                        worksheet.Cells[j, 3] = gridSpam.Rows[i].Cells[cSubject.Index].FormattedValue.ToString();
                        worksheet.Cells[j, 4] = senderEmail;
                        j++; //Ten indeks jest potrzebny, żeby pominięte duplikaty nie zostawiały pustych wierszy w Excelu
                    }
                }

                //Zapisywanie arkusza (dokładniej skoroszytu) Excel na Pulpicie

                workbook.SaveAs(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\Spam.xls",
                    Type.Missing, Type.Missing, Type.Missing, Type.Missing,
                    Type.Missing, Excel.XlSaveAsAccessMode.xlExclusive,
                    Type.Missing, Type.Missing, Type.Missing, Type.Missing);
                app.Quit();

                //A tu inne nawiązanie do ShellExecute - z podaniem ścieżki

                System.Diagnostics.Process pRun = new System.Diagnostics.Process();
                pRun.StartInfo.FileName = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "\\Spam.xls";
                pRun.Start();
            }
            catch (System.Runtime.InteropServices.COMException ex)
            {
                MessageBox.Show(ex.ToString());
            }
            finally
            {
                app = null;
                workbook = null;
                worksheet = null;
            }
        }

        //Funkcja sprawdzająca, czy nadawca nie znajduje się na liście adresów chroninych
        //Zdarza się, że jako nadawcę spamu spamerzy wpisują adres firmy, w której pracujesz

        bool inProtectList(string spamSenderEmail)
        {
            bool result = false;
            ArrayList protectedSenderEmail = new ArrayList();
            foreach (string line in richEditProtectAdr.Lines)
            {
                if (line.IndexOf("//") < 0) //Jeśli potrzbujesz komentarzy na liście (jako początek linii)
                {
                    protectedSenderEmail.Add(line);
                }
            }
            for (int i = 0; i < protectedSenderEmail.Count; i++)
            {
                //Tak rozbudowany if, bo łatwo o pomyłkę
                //przy tworzeniu listy adresów chronionych

                if (spamSenderEmail.Trim().ToLower() == protectedSenderEmail[i].ToString().ToLower().Trim())
                {
                    result = true;
                    break;
                }
            }
            return result;
        }

        string Coalesce(string s)
        {
            if (s == null)
            {
                s = "";
            }
            return s;
        }

        void bGetFolderExplicit_Click(object sender, EventArgs e)
        {
            GetSpam(SpamFolderSelcectOption.optFolderExplicit);
        }

        void bGetAnyUserFolder_Click(object sender, EventArgs e)
        {
            GetSpam(SpamFolderSelcectOption.optAnyUserFolder);
        }

        void FormSpamSelector_Load(object sender, EventArgs e)
        {
            try
            {
                richEditProtectAdr.LoadFile(Application.StartupPath + "\\AdresyChronione.txt", RichTextBoxStreamType.PlainText);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

        void FormSpamSelector_FormClosing(object sender, FormClosingEventArgs e)
        {
            try
            {
                richEditProtectAdr.SaveFile(Application.StartupPath + "\\AdresyChronione.txt", RichTextBoxStreamType.PlainText);
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
    }
}

1 komentarz

Dawno tu nie byłem. Pozdrowienia dla społeczności.