Microsoft Outlook troubleshooting
Zdarzenia SMTP w procesie przetwarzania wiadomości przez serwer SMTP (cz.II)

Zdarzenia SMTP w procesie przetwarzania wiadomości przez serwer SMTP (cz.II)

autor CodeTwo 2006-05-15 00:00:00 w Exchange Server

Artykuł dotyczy:

- Windows SMTP Server 2000/2003
- Exchange Server 2000/2003


Spis treści:

Wstęp
Tworzenie własnych odbiorców zdarzeń
Rejestracja odbiorcy zdarzeń
Ograniczenia
Przetwarzanie wiadomości pocztowej w środowisku Exchange Server'a
Wnioski
Podsumowanie
Dodatkowa literatura



Wstęp

W poprzednim artykule przedstawiłem proces przetwarzania wiadomości przez serwer SMTP i związane z tym zdarzenia, które mogą być wykorzystywane przez komponenty COM (odbiorców zdarzeń) do rozszerzania funkcjonalności serwera SMTP. Pokazałem również, na czym polega rejestracja odbiorców zdarzeń i w jaki sposób można zarejestrować własnego odbiorcę.

W tej części chciałbym przedstawić więcej informacji praktycznych, czyli przede wszystkim, jak można napisać własnego odbiorcę zdarzeń i jak go uruchomić. Aby jednak zrozumieć pewne zachowania mające wpływ na praktyczne wykorzystanie odbiorców, będziemy musieli bliżej przyjrzeć się procesowi przepływu wiadomości pocztowych przez serwer SMTP w środowisku Exchange Serwer'a.


Tworzenie własnych odbiorców zdarzeń

W poprzedniej części artykułu przedstawiłem wszystkie zdarzenia, jakie mogą być implementowane przez odbiorców zdarzeń. Zaliczamy do nich:

Zdarzenia protokołu SMTP:
- OnInboundCommand
- OnOutboundCommand
- OnServerResponse
Zdarzenia transportowe:
- OnSubmission
- OnPreCategorize
- OnPostCategorize
- OnMaxMsgSize
- OnMsgTrackLog
- OnEventLog
Zdarzenia systemowe:
- OnCategorize
- OnGetMessageRouter
- StoreDriver
- OnDnsResolveRecord


Jest ich sporo i pozwalają znacznie rozszerzać i zmieniać funkcjonalność serwera SMTP. Najczęściej do tworzenia własnych rozwiązań wykorzystywane są jednak zdarzenia protokołu SMTP oraz zdarzenia transportowe OnSubmission i OnPostCategorize ze względu na w miarę łatwe możliwości ich implementacji. W artykule chciałbym pokazać proste przykłady tworzenia własnych odbiorców zdarzeń. Do tego celu z racji prostoty i czytelności najlepiej nadaje się język VBScript. Ponieważ VBScript jest językiem skryptowym, więc nie można w nim używać obiektów implementujących interfejsy binarne (podobnie jak w VisualBasic), a takie są wykorzystywane w zdarzeniach SMTP. W związku z tym odbiorca zdarzeń implementujący w języku skryptowym interfejs wymagany przez dane zdarzenie, byłby bezużyteczny, ponieważ nie miałby dostępu do żadnych danych. Z pomocą przychodzi nam tutaj biblioteka CDO, która implementuje w jednym z komponentów COM własnego odbiorcę zdarzeń OnSubmission i pozwala wykorzystać go do wykonywania kodu napisanego w VBScript. Biblioteka CDO wystawia interfejs CDO.ISMTPOnArrival (w CDO zdarzenie OnSubmission nazywane jest OnArrival), który wystarczy zaimplementować i odpowiednio zarejestrować, aby nasz "skryptowy" odbiorca zdarzeń zaczął działać.

Napiszmy więc pierwszy skrypt, który zapisuje do pliku nadawcę, odbiorcę i tytuł każdej wiadomości przetwarzanej przez serwer SMTP. Oto on:

<SCRIPT LANGUAGE= "VBScript">

Const cdoRunNextSink = 0
Sub ISMTPOnArrival_OnArrival( ByVal Msg, EventStatus )

    ' Otwórz plik 
    Set fs = CreateObject("Scripting.FileSystemObject")	
    Set file = fs.OpenTextFile("C:\Log.txt", 8, True )

    ' Zapisz dane do pliku
    file.Write "From: " & Msg.From & vbCrLf
    file.Write "To: " & Msg.To & vbCrLf
    file.Write "Subject: " & Msg.Subject & vbCrLf

    EventStatus = cdoRunNextSink
End Sub

</SCRIPT>

Jak napisałem wcześniej, odbiorca musi implementować interfejs ISMTPOnArrival wystawiany przez CDO. Jest w nim tylko jedna funkcja OnArrival, wołana za każdym razem, gdy do warstwy transportowej zostaje dostarczona nowa wiadomość. Wiadomość została już odebrana od zdalnego serwera lub klienta przez warstwę protokołu serwera i właśnie będzie wstawiana do kolejki wiadomości oczekujących na kategoryzację. Przedtem jednak serwer SMTP daje nam szansę przetworzenia jej w naszym odbiorcy zdarzeń. Nieco dalej zobaczymy, co trzeba zrobić, aby serwer SMTP wykonywał nasz skrypt.

Funkcja OnArrival ma dwa argumenty, pierwszy to przetwarzana wiadomość przekazana jako obiekt CDO.IMessage, drugi to wynik przetwarzania zwracany przez nasz skrypt. Możemy zwrócić wartość cdoRunNextSink (jest to wartość domyślna), co znaczy, że wiadomość ma być dalej normalnie przetwarzana przez pozostałych odbiorców zdarzeń, lub cdoSkipRemainingSinks, jeśli chcemy, aby wiadomość nie była przetwarzana przez pozostałych odbiorców. Ponieważ wartość cdoRunNextSink jest domyślna, więc równie dobrze możemy opuścić instrukcję EventStatus = cdoRunNextSink.

W powyższym skrypcie otwieramy najpierw plik tekstowy a potem zapisujemy do niego adres nadawcy, odbiorcy oraz tytuł. Do pobrania tych danych wykorzystujemy pola przekazanego jako pierwszy argument obiektu CDO.IMessage - odpowiednio From, To, Subject. W ten sposób udało nam się napisać prostego odbiorcę zdarzeń ewidencjonującego wszystkie wiadomości przetwarzane przez nasz serwer. Dane te możemy wykorzystać później na przykład do tworzenia statystyk ruchu poczty i obciążenia serwera. Wypada jeszcze uzupełnić go o dodatkowe informacje, na przykład takie jak czas wysłania czy otrzymania wiadomości, liczba i rozmiar załączników.

Należy tu jeszcze raz podkreślić, że funkcja OnArrival jest wołana dla każdej wiadomości dostarczanej do warstwy transportowej serwera SMTP. Zarówno dla wiadomości przychodzących jak i wychodzących, czyli tych które dostarczane są przez serwer lokalnie i tych dostarczanych zdalnie - przesyłanych do innych serwerów SMTP. Z resztą w miejscu, w którym jesteśmy - w zdarzeniu OnSubmission - nie istnieje pojęcie wiadomości przychodzącej czy wychodzącej. Po prostu serwer przetwarza pewną jeszcze nie skategoryzowaną wiadomość pocztowa i nie wie czy powinna ona zostać dostarczona do lokalnej skrzynki czy wysłana do innego serwera pocztowego. Oczywiście nic nie stoi na przeszkodzie (poza wydajnością), abyśmy sami odczytali adres nadawcy i odbiorcy i stwierdzili na podstawie danych w Active Directory, czy jest to wiadomość wysłana do lokalnego użytkownika czy nie. Tak też należy uczynić przy implementacji pewnych rozwiązań, na przykład, gdy chcemy dodawać stopkę do wszystkich maili wysyłanych w świat przez pracowników naszej firmy, ale o tym później.

Przyjrzyjmy się głównym polom (właściwościom) obiektu CDO.IMessage, które możemy wykorzystać w odbiorcy zdarzeń:

Nazwa pola Opis
Attachments Załączniki wiadomości. Obiekt klasy CDO.IBodyParts, który jest z kolei kolekcją obiektów CDO.IBodyPart. Funkcja IBodyPart::GetStream zwraca obiekt IStream, z którego można przeczytać zawartość załącznika. Patrz dokumentacja "CDO for Windows 2000".
BCC Adresy odbiorców typu "Blind carbon copy".
CC Adresy odbiorców typu "Carbon copy".
EnvelopeFields Obiekt typu ADODB.Fields zawierający dane związane z transportem wiadomości. Patrz opis dalej w tekście.
Fields Obiekt typu ADODB.Fields, przez który można odczytać wszystkie nagłówki wiadomości. Patrz opis dalej w tekście.
From Adres nadawcy wiadomości. Jest to wartość nagłówka From w wiadomości. Jeśli nagłówek From jest pusty to wstawiana jest tu wartość podana w komendzie SMTP: MAIL FROM.
HTMLBody Treść wiadomości w formacie HTML. Jeśli wiadomość nie jest w formacie HTML, pole to jest puste.
MDNRequested Wartość typu Boolean. Określa, czy nadawca wiadomości zażądał potwierdzenia o jej odczytaniu.
MimeFormatted Wartość typu Boolean. Określa, czy wiadomość jest sformatowana jako MIME multipart/alternative.
ReceivedTime Data odebrania wiadomości ustawiana na podstawie ostatniego nagłówka Received.
ReplyTo Adres na jaki mają być wysyłane odpowiedzi na tą wiadomość.
SentOn Data wysłania wiadomości ustawiana na podstawie nagłówka Date.
Subject Tytuł wiadomości
TextBody Tekst wiadomości. Jeśli jest to wiadomość w formacie HTML to pole to przedstawia jego tekstową reprezentację bez znaczników HTML.
To Adres odbiorcy wiadomości ustalany na podstawie nagłówka To.


Dodatkowego omówienia wymagają obiekty Fields i EnvelopeFields. Są to kolekcje pól, z których można odczytać dodatkowe informacje o wiadomości. Poprzez obiekt Fields możemy odczytać wartości wszystkich nagłówków RFC wiadomości. W tym celu należy użyć przestrzeni nazw urn:schemas:mailheader: z dodaną nazwą nagłówka. Na przykład, aby odczytać tytuł wiadomości, który zapisany jest w nagłówku Subject, należy użyć poniższego kodu:

strSubject = Msg.Fields("urn:schemas:mailheader:Subject")

W ten sposób możemy odczytywać również nagłówki niestandardowe, np.: x-spamlevel:

strSubject = Msg.Fields("urn:schemas:mailheader:x-spamlevel")

Obiekt EnvelopeFields umożliwia nam dostęp do danych, które nie są bezpośrednio zawarte w wiadomości, ale związane są z jej transportem przez serwer SMTP, np. czas dostarczenia wiadomości, czy adres klienta. Aby odczytać ich wartość należy użyć przestrzeni nazw http://schemas.microsoft.com/cdo/smtpenvelope/

Nazwa pola Opis
arrivaltime Data kiedy wiadomość została dostarczona do naszego serwera SMTP.
clientipaddress Adres IP klienta, od którego wiadomość została odebrana.
messagestatus Status wiadomości. Może mieć trzy wartości:
cdoStatSuccess (0) - kontynuuj dostarczanie wiadomości
cdoStatAbortDelivery (2) - odrzuć wiadomość i jej nie dostarczaj
cdoStatBadMail (3) - nie dostarczaj wiadomości i umieść ją w katalogu BadMail.
pickupfilename Jeśli wiadomość została dostarczona do serwera poprzez skopiowanie pliku do foldera Pickup, pole to zawiera pełną ścieżkę do nazwy pliku.
recipientlist Lista odbiorców wiadomości, którzy zostali podani w komendzie RCPT TO. Jeśli wiadomość nie została dostarczona do serwera przez protokół SMTP, pole to jest puste.
senderemailaddress Adres nadawcy wiadomości podany w komendzie MAIL FROM. Jeśli wiadomość nie została dostarczona do serwera przez protokół SMTP to pole to jest puste.


Na szczególną uwagę zasługują tu pola messagestatus i recipientlist, ponieważ dzięki nim mamy wpływ na to jak wiadomość jest przetwarzana. Ustawiając odpowiednio wartość pola messagestatus możemy spowodować, że wiadomość zostanie odrzucona i nie będzie dalej przetwarzana przez serwer. Poniższy skrypt powoduje, że serwer SMTP odrzuca wszystkie wiadomości odbierane od użytkowników z domeny @firma.com. Ponieważ żądamy od serwera zaprzestania dalszego przetwarzania wiadomości, dlatego nie zostanie wygenerowany raport NDR (non delivery report) o niedostarczeniu.

<SCRIPT LANGUAGE="VBScript">

Const cdoStatAbortDelivery = 2
Const cdoSkipRemainingSinks = 1

Sub ISMTPOnArrival_OnArrival( ByVal Msg, EventStatus )

    ' Pobierz adres nadawcy
    strSenderAddr = Msg.EnvelopeFields("http://schemas.microsoft.com/cdo/smtpenvelope/senderemailaddress")
    If InStr(strSenderAddr, "@firma.com") > 0 Then
		
        ' Tych Państwa nie obsługujemy
        Msg.EnvelopeFields("http://schemas.microsoft.com/cdo/smtpenvelope/messagestatus") = cdoStatAbortDelivery
		
        ' Zapisz zmiany
        Msg.EnvelopeFields.Update
        EventStatus = cdoSkipRemainingSinks
    End If
End Sub

</SCRIPT>

Argument EventStatus ustawiamy na wartość cdoSkipRemainingSinks, aby powiadomić dispatchera zdarzeń, że wiadomość nie musi być przetwarzana przez pozostałych odbiorców. Nie ma takiej potrzeby skoro wiadomość i tak zostanie przez serwer odrzucona.

Pole recipientlist używamy do sprawdzenia odbiorców wiadomości. Możemy przy jego pomocy również usunąć lub dodać dodatkowego odbiorcę. Dzięki temu możemy na przykład stworzyć odbiorcę zdarzeń, wysyłającego wszystkie wiadomości odebrane od użytkowników na dodatkowy adres SMTP. Jest to często poszukiwane rozwiązanie, bo o ile serwer Exchange pozwala określić dodatkowego odbiorcę dla wiadomości dostarczanej do skrzynki użytkownika, o tyle nie można określić dodatkowego odbiorcy dla wiadomości wysłanych przez użytkowników. Problem ten pojawia się szczególnie w przypadku zaostrzonych procedur nadzoru, gdy konieczne jest monitorowanie całego ruchu e-mail w firmie. Poniższy skrypt najpierw sprawdza czy wiadomość została wysłana przez użytkownika z naszej domeny, tu @mojafirma.com. Jeśli tak to czyta adresy obecnych odbiorców wiadomości i dodaje do nich dodatkowy adres archive@mojafirma.com. W wyniku tego każda wiadomość wysyłana przez użytkowników naszego serwera zostanie dodatkowo przesłana na wskazany adres. Ponieważ adres dodajemy do pól transportowych EnvelopeFields, to nie będzie on widoczny w treści wiadomości i użytkownik, który odbierze wiadomość nie będzie wiedział, że została ona wysłana także na adres archive@mojafirma.com.

<SCRIPT LANGUAGE="VBScript">

Sub ISMTPOnArrival_OnArrival( ByVal Msg, EventStatus )

    On Error Resume Next

    ' Sprawdź czy mail wysyłany z naszej firmy
    strFromAddr = Msg.From

    If InStr( strFromAddr, "@mojafirma.com" ) > 0 Then
		
        ' Przeczytaj adresy odbiorców
        Set RecipientsField = Msg.EnvelopeFields("http://schemas.microsoft.com/cdo/smtpenvelope/recipientlist")
        strRecipients = RecipientsField.Value
		
        ' Dodaj dodatkowego odbiorcę
        If Len(strRecipients) > 0 Then strRecipients = strRecipients & ";"
        strRecipients = strRecipients & "SMTP:archive@mojafirma.com"
        RecipientsField.Value = strRecipients

        ' Zapisz zmiany
        Msg.EnvelopeFields.Update
    End If
End Sub

</SCRIPT>

Jeszcze jedna ważna kwestia dotycząca wydajności. Obecnie nasz odbiorca zdarzeń jest tworzony i ładowany przez dispatcher'a zdarzeń za każdym razem, gdy występuje zdarzenie OnSubmission. Aby tego uniknąć możemy zaimplementować interfejs IEventIsCacheable. Wtedy raz utworzony obiekt naszego odbiorcy zostanie zbuforowany i będzie wykorzystywany wielokrotnie dla obsłużenia wielu zdarzeń. Implementacja interfejsu IEventIsCacheable jest wyjątkowo łatwa. Posiada on tylko jedną funkcję IsCacheable, która powinna zwrócić wartość S_OK, jeśli odbiorca ma być buforowany. Nasz pierwszy przykład z zaimplementowanym interfejsem IEventIsCacheable wyglądałby w ten sposób

<SCRIPT LANGUAGE= "VBScript">  Private Sub IEventIsCacheable_IsCacheable()     ' taka implementacja w VBScript zwraca S_OK End Sub  Sub ISMTPOnArrival_OnArrival( ByVal Msg, EventStatus )      ' Otwórz plik      Set fs = CreateObject("Scripting.FileSystemObject")	     Set file = fs.OpenTextFile("C:\Log.txt", 8, True )      ' Zapisz dane do pliku     file.Write "From: " & Msg.From & vbCrLf     file.Write "To: " & Msg.To & vbCrLf     file.Write "Subject: " & Msg.Subject & vbCrLf End Sub  </SCRIPT> 


Rejestracja odbiorcy zdarzeń

Wiemy już jak napisać prostego odbiorcę zdarzeń, zobaczmy teraz jak powiadomić serwer SMTP o jego istnieniu i spowodować, aby serwer wprowadził go do użytku. Najpierw należy zapisać kod skryptu w pliku tekstowym. Ponieważ jest on napisany w VBScript, zapiszmy go z rozszerzeniem .vbs w pliku c:\sink.vbs. Teraz użyjemy skryptu smtpreg.vbs do zarejestrowania naszego odbiorcy w następujący sposób:

cscript smtpreg.vbs /add 1 OnArrival MySmtpSink CDO.SS_SMTPOnArrivalSink "MAIL FROM=*" cscript smtpreg.vbs /setprop 1 OnArrival MySmtpSink Sink ScriptName "c: \sink.vbs"

Pierwsze wywołanie skryptu smtpreg.vbs rejestruje nowego odbiorcę zdarzenia OnArrival (to samo co OnSubmission, tylko pod inną nazwą) o nazwie MySmtpSink. Argument MAIL FROM=*, to filtr określający, dla jakich wiadomości uruchamiany ma być nasz odbiorca zdarzeń. W tym wypadku powinien być uruchamiany dla wszystkich wiadomości, niezależnie od tego kto jest jej nadawcą. Argument liczbowy 1 określa, dla której instancji serwera SMTP zainstalowany ma być nasz odbiorca. Jeśli w systemie jest więcej niż jedna instancja serwera to otrzymuje ona kolejny numer 2,3,4 itd. Argument CDO.SS_SMTPOnArrivalSink określa obiekt COM, który implementuje odbiorcę zdarzeń - o tym za moment. W drugim wywołaniu skryptu smtpreg.vbs ustawiamy zarejestrowanemu przed chwilą odbiorcy jeden argument o nazwie ScriptName na wartość c:\sink.vbs - czyli ścieżkę do naszego skryptu.

W pierwszym wywołaniu skryptu smtpreg.vbs tak naprawdę rejestrujemy nowego odbiorcę zdarzeń, który jest implementowany przez bibliotekę CDO. Natomiast w drugim wywołaniu przekazujemy mu ścieżkę do naszego skryptu VBScript. Obiekt COM CDO.SS_SMTPOnArrivalSink implementuje odbiorcę zdarzenia OnSubmission właśnie po to, aby umożliwić uruchamianie odbiorców zdarzeń napisanych w językach skryptowych. Jak wspomniałem na wstępie, skryptowy odbiorca zdarzeń nie ma dostępu do interfejsów binarnych przekazywanych w zdarzeniach SMTP. CDO jest tu pomostem pomiędzy dispatcherem zdarzeń a naszym skryptem. Dispatcher zdarzeń serwera SMTP wysyła powiadomienie o zdarzeniu OnSubmission do odbiorcy implementowanego przez obiekt z biblioteki CDO, a ten na podstawie przekazanej mu ścieżki, wykonuje kod skryptu z odpowiedniego pliku.

Po zarejestrowaniu odbiorcy musimy zrestartować usługę SMTP, aby nasz odbiorca zaczął być wywoływany.

Uwaga: Zarejestrowanie odbiorcy zdarzeń z regułą filtrującą MAIL FROM=* (jak i każdą inną) spowoduje, że nie będzie on uruchamiany dla wiadomości wysyłanych przy pomocy Microsoft Outlook, OWA, CDO. Problem ten wyjaśniony jest w dalszej części artykułu.

Po zarejestrowaniu odbiorcy zdarzeń możemy uruchomić ponownie skrypt smtpreg.vbs tym razem z argumentem /enum, aby poszukać wpisu dotyczącego naszej rejestracji w metabazie serwera IIS. Ma on następującą postać:

-----------
| Binding |
-----------
    Event: SMTP Transport OnSubmission
    ID: {C1510B94-9C43-4AAB-9B41-380D012366AE}
    Name: MySmtpSink
    SinkClass: CDO.SS_SMTPOnArrivalSink
    Enabled: True
    SourceProperties: {
                        Priority = 24575
                      }
    SinkProperties:   {
                        ScriptName = c:\sink.vbs
                      }

Widzimy więc, że jest to odbiorca tak naprawdę implementowany przez obiekt CDO o nazwie klasy CDO.SS_SMTPOnArrivalSink, który w parametrach ma zdefiniowaną ścieżkę do naszego skryptu zapisanego w pliku c:\sink.vbs, dlatego wie jaki skrypt ma wykonywać.

Jeśli chcemy odrejestrować naszego odbiorcę zdarzeń musimy uruchomić skrypt smtpreg.vbs w z następującymi parametrami:

cscript smtpreg.vbs /remove 1 onarrival MySmtpSink


Ograniczenia

Do tej pory wszystko przebiegało gładko. Napisaliśmy zaledwie kilka linii prostego kodu, a udało nam się zaimplementować całkiem pożyteczną funkcjonalność w naszym serwerze SMTP. Krótki skrypt wystarcza, aby kazać serwerowi SMTP odrzucać wybrane wiadomości, czy przesyłać całą korespondencję do dodatkowego wybranego odbiorcy. Niestety kolejny, jeden z najpowszechniejszych i często poszukiwanych przykładów, pokaże nam pewne problemy wiążące się z przede wszystkim skryptowymi odbiorcami zdarzeń. Jednocześnie zmusi nas do nieco dokładniejszego przyglądnięcia się procesowi przesyłania wiadomości w serwerze Exchange.

Tym przykładem będzie dodawanie stopki do wiadomości wysyłanych przez użytkowników naszego serwera. Prezentuje go poniższy skrypt:

<SCRIPT LANGUAGE="VBScript">

Sub ISMTPOnArrival_OnArrival( ByVal Msg, EventStatus )
	On Error Resume Next

	' Sprawdź czy mail wysyłany z naszej firmy
	strFromAddr = Msg.From
	If InStr( strFromAddr, "@mojafirma.com" ) > 0 Then

		' Dodaj stopkę do wiadomości
		Msg.TextBody = Msg.TextBody & vbCrLf & "Oto nasza firmowa stopka"

		' Zapisz zmiany
		Msg.DataSource.Save
	End If
End Sub

</SCRIPT>

Jest to maksymalnie uproszczony przykład dodawania stopki do wysyłanych wiadomości. Najpierw sprawdzamy czy adres nadawcy należy do użytkownika naszego serwera, następnie dodajemy stopkę do treści wiadomości i zapisujemy zmiany. Stopka jest dodawana tylko do czysto tekstowej reprezentacji treści wiadomości. Jeśli chcemy dodawać ją również do wiadomości w formacie HTML to należy zmieniać zawartość pola HTMLBody wstawiając stopkę przed znacznikiem </body>.

Gdy zarejestrujemy powyższy skrypt i rozpoczniemy testy okaże się, że nie działa on w pewnych okolicznościach. A mianowicie gdy użytkownik posiadający skrzynkę na tym serwerze wysyła wiadomość używając Microsoft Outlook, OWA czy samego CDO. W środowisku Exchange są to raczej często występujące okoliczności. Dodam tu, że w przypadku Microsoft Outlook chodzi o wysyłanie wiadomości bezpośrednio przez skrzynkę Exchange skonfigurowaną w profilu, wtedy Outlook jest dla Exchange Server'a klientem MAPI.

Istnieją dwie przyczyny takiego zachowania. Pierwsza to ta, że w tym wypadku nasz skrypt, podobnie jak wszystkie poprzednie, nie zostanie w ogóle wywołany z powodu reguły, jaką zastosowaliśmy do jego rejestracji, a mianowicie MAIL FROM=*. Problem nie leży w konkretnej użytej komendzie SMTP, lecz w tym, że jeśli chcemy, aby nasze skrypty były wołane dla wiadomości wysyłanych z klientów MAPI, OWA, CDO, to nie możemy do ich rejestracji użyć żadnej reguły filtrującej - musimy zastosować pustą regułę. A to dlatego, że tego typu wiadomości nie są dostarczane do serwera SMTP przez protokół SMTP, lecz bezpośrednio do warstwy transportowej przez serwer Exchange. W związku z tym nie ma żadnej wymiany komend SMTP i dispatcher zdarzeń nie może dopasować żadnej z nich do naszej reguły filtrującej. Pozostaje nam więc użycie pustej reguły. To jednak ma z kolei negatywny wpływ na obciążenie serwera SMTP, ponieważ musi wołać nasz mocno niewydajny skrypt dla każdej przetwarzanej wiadomości pocztowej. Widzimy więc, że użycie skryptowych odbiorców zdarzeń niesie za sobą pewne poważne konsekwencje, które wymagają dokładnego rozpatrzenia w zależności od charakterystyki środowiska w jakim będą używane. Jeszcze jedna uwaga. Aby zarejestrować odbiorcę z pustą regułą należy wywołać skrypt smtpreg.vbs z następującymi parametrami:

cscript smtpreg.vbs /add 1 OnArrival MySmtpSink CDO.SS_SMTPOnArrivalSink ""

Skrypt smtpreg.vbs pobrany ze stron MSDN rejestruje jednak błędnie odbiorcę zdarzeń z pustą regułą, w wyniku czego nigdy nie jest on uruchamiany. Poprawnie działający skrypt do rejestracji należy pobrać z https://www.outlook.pl/downloads/smtpreg.vbs

Udało nam się spowodować, że nasze przykładowe skrypty są zawsze wykonywane dla zdarzenia OnSubmission niezależnie od klienta używanego przez użytkownika do wysyłania wiadomości i teraz działają bez zarzutu. Z jednym jednak wyjątkiem. Mimo że teraz nasz skrypt jest wykonywany, to stopka wciąż nie jest dodawana do wiadomości wysyłanych przy pomocy Microsoft Outlook, OWA, CDO. Niestety jest to problem, którego nie da się rozwiązać w skryptowym odbiorcy zdarzeń. Nie można w zdarzeniu OnSubmission zmienić zawartości wiadomości pocztowej, która jest wysłana przy pomocy tych klientów. A dokładniej, nie można zmienić zawartości wiadomości, które zostały dostarczone do warstwy transportowej serwera SMTP przez komponent store driver serwera Exchange. Aby zrozumieć dlaczego tak się dzieje, musimy bliżej przyjrzeć się procesowi przetwarzania wiadomości przez serwer SMTP w środowisku Exchange Server'a.


Przetwarzanie wiadomości pocztowej w środowisku Exchange Server'a

W poprzedniej pierwszej części artykułu przedstawiłem przepływ wiadomości pocztowej przez serwer SMTP w przypadku, gdy wiadomość jest dostarczana i wysyłana z serwera przez warstwę (stos) protokołu SMTP. Jednak w środowisku Exchange wiadomości mogą być dostarczane do serwera również innymi drogami, co obrazuje poniższy rysunek:

Przepływ wiadomości w serwerze SMTP w środowisku Exchange Server'a
rys.1 Przepływ wiadomości w serwerze SMTP w środowisku Exchange Server'a


Na rysunku widzimy, że protokół SMTP jest tylko jednym ze źródeł, przez które do serwera pocztowego może trafić wiadomość. Dla innych źródeł, takich jak:

- Exchange 5.5,
- Konektory: X.400, Lotus Notes, Novell GroupWise,
- Inne konektory firm trzecich,
- Microsoft Outlook, OWA, CDO

wiadomości są dostarczane do serwera Exchange bezpośrednio lub przez agenta Exchange MTA z pominięciem protokołu SMTP. W takim wypadku fizyczna reprezentacja wiadomości, czyli obiekt MailMsg jest dostarczany przez store driver'a serwera Exchange, a ten ma to do siebie, że tworzy wiadomość w formacie MAPI. Z drugiej strony każda wiadomość w systemie musi przejść przez warstwę transportową serwera SMTP (Advanced Queueing Engine), aby po sprawdzeniu jej nadawcy i odbiorców zadecydować czy ma zostać dostarczona lokalnie czy zdalnie, czy też mają zostać zastosowane do niej specjalne reguły, na przykład gdy przekracza dozwolony rozmiar. Każda wiadomość musi również przejść przez warstwę transportową, aby umożliwić jej śledzenie, oraz poinformować zarejestrowanych odbiorców zdarzeń o jej istnieniu i pozwolić im podjąć odpowiednią akcję. Przykładem niech będą tu nasze skrypty, które odrzucają pewne wiadomości czy też dodają dodatkowego odbiorcę. Cały problem polega na tym, że warstwa transportowa serwera SMTP nie działa na wiadomości w formacie MAPI lecz RFC. Dlatego store driver Exchange'a tworzy kopię wiadomości MAPI w formacie RFC i przekazuje ją do kolejki wiadomości odebranych. Kopia ta ma to do siebie, że nie zawiera części istotnych danych. Nie ma adresu nadawcy SMTP, bo taki nie istnieje, podobnie nie ma adresów odbiorców, jeśli są to adresy inne niż SMTP. Treść wiadomości jest dostępna, ale dostarczana jest dopiero, gdy zażąda tego któryś z odbiorców zdarzeń. W praktyce oznacza to, że w naszym skrypcie w ogóle nie odczytamy treści wiadomości, bo CDO, gdy tworzy obiekt CDO.IMessage nie żąda od store driver'a jej treści.

Kopia wiadomości przechowywanej przez store driver serwera Exchange dociera do komponentu kategoryzatora i ten na podstawie adresu odbiorcy decyduje czy wiadomość powinna zostać dostarczona do lokalnej skrzynki na serwerze, czy powinna zostać przesłana do innego serwera. Jeśli wiadomość ma zostać przesłana dalej, to kategoryzator sprawdza, czy powędruje ona do innego serwera w organizacji, czy też powinna zostać wysłana do serwera zewnętrznego. Jest to ważna decyzja ze względu na to, że pełna treść wiadomości jest wciąż przechowywana w formacie MAPI, a w tym momencie kategoryzator musi zadecydować, w jakim formacie przesłać wiadomość dalej. Jeśli jest to serwer należący do organizacji Exchange to wiadomość może zostać przesłana w formacie MAPI. Dokładniej mówiąc jest to format TNEF (Transport Neutral Encapsulation Format), w którym do wiadomości RFC 822 dołączony jest załącznik winmail.dat zawierający spakowaną jest wiadomość MAPI. Jeśli wiadomość ma zostać przesłana do serwera zewnętrznego to musi ona cała zostać skonwertowana z formatu MAPI na format RFC 822, bo klienci serwera docelowego mogą nie mieć możliwości odczytać wiadomości MAPI. W rzeczywistości to w jakim formacie wiadomość zostanie wysłana do zewnętrznego odbiorcy zależy jeszcze od ustawień w Exchange Managerze (Global Settings | Internet MessageFormats) oraz od ustawień formatu wiadomości dla danego odbiorcy w Outlook'u. Jednak kwestia, która tutaj najbardziej nas interesuje to to, że zarówno gdy kategoryzator żąda od store driver'a stworzenia wiadomości w formacie TNEF czy też skonwertowania jej z MAPI na RFC, to store driver dokonuje tych operacji na podstawie oryginalnej wiadomości MAPI którą przechowuje, a nie na podstawie kopii, którą wysłał do warstwy transportowej serwera SMTP. W związku z tym wszelkie zmiany dokonane na treści wiadomości przez odbiorców zdarzeń zostają utracone. To powinno wyjaśnić nam, dlaczego w zdarzeniu OnSubmission nie można między innymi dodać stopki do wiadomości przesyłanej wewnątrz organizacji. Po prostu pracujemy na kopii wiadomości odrzuconej w momencie tworzenia wiadomości, która faktycznie zostanie wysłana do odbiorcy. Jednocześnie zauważmy, że pola EnvelopeFields nie są bezpośrednio związane z treścią wiadomości, lecz z procesem jej przetwarzania, dlatego zmiany dokonane np. w polu recipientlist mają wpływ na to, jak jest ona przetwarzana przez kategoryzator. Jeśli do recipientlist dodamy adres zewnętrznego odbiorcy, to kategoryzator zażąda od store driver'a skonwertowania wiadomości z formatu MAPI na RFC, aby można ją było bezpiecznie do niego wysłać.

Gdy wiadomość ma zostać dostarczona do lokalnej skrzynki, która znajduje się w tym samym magazynie skrzynek (mailbox store) co skrzynka nadawcy, to kategoryzator nie wymaga od store driver'a żadnej konwersji. Wiadomość zostaje wstawiona do kolejki dostarczania lokalnego i zostaje umieszczona w odpowiedniej skrzynce. W tym wypadku również wykorzystana zostaje oryginalna wiadomość przechowywana przez store driver'a a nie jej kopia przetwarzana w warstwie transportowej. Jeśli wiadomość ma zostać dostarczona lokalnie, ale do skrzynki znajdującej się w innym magazynie skrzynek, to kategoryzator żąda dostarczenia wiadomości w formacie TNEF.

Ciekawym zagadnieniem jest tutaj przypadek, gdy wiadomość ma odbiorców, do których dostarczona musi zostać w różnych formatach. Na przykład, gdy użytkownik wysyła z Outlook'a wiadomość do odbiorcy wewnętrznego z organizacji oraz do odbiorcy zewnętrznego. W takim przypadku kategoryzator żąda dostarczenia dwóch fizycznych wiadomości jednej w formacie TNEF, drugiej skonwertowanej do postaci RFC. Faktycznie dochodzi więc do fizycznego powielenia wiadomości, w terminologii technicznej proces ten nazywany jest w języku angielskim bifurcation.

Jak zauważyliśmy, nie jest możliwa zmiana treści wiadomości w zdarzeniu OnSubmission, ponieważ nie pracujemy z obiektem wiadomości, który zostanie przesłany do odbiorcy. Jednak taka możliwość pojawia się w zdarzeniu OnPostCategorize, którego nie można jednak zaimplementować przy użyciu języka skryptowego typu VBScript czy Visual Basic. Tutaj jest już po kategoryzacji i nasz odbiorca ma szansę pracować na skonwertowanej wiadomości. Jednak wciąż istnieją dwa problemy. Pierwszy to przypadek, gdy wiadomość jest przesyłana do odbiorcy, który ma skrzynkę w tej samym magazynie skrzynek co nadawca. Wtedy wciąż pracujemy z kopią wiadomości, która zostanie odrzucona przez store driver'a w momencie zapisywania wiadomości w Information Store. Tu nie ma żadnego rozwiązania. Drugi problem dotyczy wiadomości skonwertowanych do formatu TNEF. Jeśli chcemy w jakiś sposób dokonać zmian na przesyłanej wiadomości to musimy pamiętać, że wiadomością tą jest tak naprawdę załącznik binarny winmail.dat. Musimy więc dokładnie znać jego format, który z tego co wiem nie jest udokumentowany. Z tego powodu ten problem jest również trudny do rozwiązania. Jednak rejestrując własnego odbiorcę dla zdarzenia OnPostCategorize sporo zyskujemy w porównaniu do zdarzenia OnSubmission. Możemy zmieniać zawartość, w tym dodawać stopkę do wszystkich wiadomości, które wysyłane są do odbiorców zewnętrznych w formacie tekstowym i HTML, a więc zazwyczaj do wszystkich odbiorców nie należących do organizacji Exchange. Zdarzenie OnPostCategorize ma jeszcze inne zalety. Ponieważ jest już po kategoryzacji, dlatego ustawione są odpowiednie pola dla obiektów nadawcy i odbiorców. Wiemy czy wiadomość jest wysyłana do odbiorcy zewnętrznego czy należącego do organizacji, nie musimy przeszukiwać Active Directory, aby to stwierdzić. Łatwo również jest rozpoznać wiadomości systemowe (na przykład replikację folderów publicznych), czy raporty NDR ponieważ mają ustawione specjalne flagi.

Jeszcze raz chcę przypomnieć, że omawiane problemy nie dotyczą przypadku, gdy wiadomość zostaje dostarczona do serwera przez protokół SMTP. Wtedy zawsze mamy możliwość edycji wiadomości, ponieważ jest ona od razu w formacie RFC i jest przez cały proces przetwarzania utrzymywana przez NTFS store driver, a nie store driver serwera Exchange. W momencie gdy wiadomość ma zostać dostarczona lokalnie do skrzynki Exchange, zostaje ona przekształcona przez store driver Exchange do formatu MAPI, aby mogła być odczytana przez użytkowników Outlook'a. Przekształcenie to jednak odbywa się na wiadomości RFC, która niesie wszystkie zmiany wprowadzone w treści przez odbiorców zdarzeń.


Wnioski

Podsumowując nasze dość długie rozważania nad tematem dodawania stopki do wiadomości (ogólnie edycji) i sposobu działania odbiorców zdarzeń, dochodzimy do następujących wniosków:

1. Odbiorca zdarzeń transportowych takich jak OnSubmission czy OnPostCategorize nie może zostać zarejestrowany z regułami filtrowania, ponieważ nie będzie on w ogóle powiadamiany o zdarzeniach, jeśli wiadomość zostanie dostarczona do serwera w inny sposób niż przez protokół SMTP.
2. Zmiana treści wiadomości jest możliwa, jeśli wiadomość została dostarczona do serwera przez protokół SMTP. W przeciwnym wypadku, gdy wiadomość została dostarczona przez konektor nie-SMTP, Microsoft Outlook, OWA, CDO - pojawiają się problemy.
3. Jeśli wiadomość nie została dostarczona do serwera przez protokół SMTP, to nie ma możliwości jej edycji w zdarzeniu OnSubmission. Oznacza to, że w takim wypadku edycja jest niemożliwa przy wykorzystaniu skryptowego odbiorcy zdarzeń.
4. Jeśli wiadomość nie została dostarczona do serwera przez protokół SMTP, to jest możliwość jej edycji w zdarzeniu OnPostCategorize, pod warunkiem jednak, że wiadomość jest skierowana do odbiorcy, dla którego zachodzi konwersja wiadomości to formatu RFC. Konwersja taka zachodzi zazwyczaj, gdy wiadomość jest wysyłana do odbiorcy spoza organizacji Exchange.



Podsumowanie

Przy pomocy skryptów VB można często w bardzo prosty sposób zrealizować zadania, które są niedostępne przy użyciu standardowych ustawień serwera pocztowego, takich jak dodawanie stopki, czy przesyłanie wychodzących wiadomości do dodatkowego odbiorcy. Mogłoby także wydawać się, że napisanie prostego skryptu może zaoszczędzić nam sporo wydatków na zakup drogiego oprogramowania implementującego zbliżoną funkcjonalność. Osobiście przestrzegam jednak przed wykorzystywaniem odbiorców zdarzeń napisanych w językach skryptowych w środowiskach produkcyjnych i zalecam dokładne ich przetestowanie przed podjęciem takiej decyzji. Skrypty są napisane w językach wysokiego poziomu i ich wykonanie pociąga za sobą duży nakład zasobów pamięci i procesora, zazwyczaj o wiele większy niż w analogicznych programach mających postać kodu maszynowego i napisanych w takich językach jak C\C++. Dlatego warto najpierw porównać na serwerze testowym, jaki wpływ na jego obciążenie ma zainstalowanie skryptu odbiorcy zdarzeń. Na pewno pozytywny wpływ na wydajność będzie miało zainstalowanie odbiorcy z odpowiednią regułą, jak najbardziej ograniczającą liczbę przetwarzanych przez niego wiadomości, to jednak z kolei powoduje, że nasz odbiorca zdarzeń nie będzie w ogóle wywoływany dla wiadomości, które są dostarczane do warstwy transportowej serwera inną drogą niż przez protokół SMTP.

Według mnie możliwość tworzenia własnych skryptowych odbiorców zdarzeń jest raczej pokazaniem tego, co można zrobić przy ich wykorzystaniu i jakie są możliwości rozszerzania serwera SMTP, niż zachętą do ich stosowania w środowiskach produkcyjnym. Taki też był cel tego artykułu. Oczywiście można wyobrazić sobie sytuacje, gdy realne jest użycie skryptowych odbiorców zdarzeń, zwłaszcza w środowiskach, które nie charakteryzują się dużym ruchem poczty. Z pewnością nie można odmówić im kilku podstawowych zalet: prostoty, łatwości i niskiej ceny wykonania.

Ponieważ od dawna zajmuję się tematyką Exchange Server'a i firmie, którą prowadzę, zdarzyło się stworzyć niejeden projekt wykorzystujący odbiorców zdarzeń serwera SMTP, dlatego postaram się w najbliższym czasie udostępnić program, który implementował będzie opisane tu przykłady. Program napisany zostanie przy użyciu języka niskiego poziomu C++, dlatego powinien być znacznie wydajniejszy od skryptów VB. Będzie również odbiorcą zdarzenia OnPostCategorize, dzięki czemu będzie mógł dodawać stopkę do wszystkich wiadomości wysyłanych na zewnątrz organizacji Exchange niezależnie od klienta pocztowego użytego przez nadawcę. Może w przyszłości uda się dodawać także inne funkcjonalności do programu, na przykład powiadomienia SMS o przychodzących wiadomościach.


Dodatkowa literatura

CDO for Windows 2000
SMTP Transport Architecture

Jeśli masz jakieś pytania lub komentarze dotyczące tego artykułu, napisz na naszym forum.

(c) CodeTwo. Wszelkie prawa zastrzeżone.



© Wszelkie prawa zastrzeżone. Żadna część ani całość tego artykułu nie może być powielana ani publikowana bez zgody autora.