PDFlibPas, losLabova razvijalska knjižnica PDF za Delphi in C++Builder, pošlje ustvarjeni PDF kot e-poštno priponko z enim ravnim klicem API-ja, SendDocumentByMail. V sistemu Windows privzeti prenos uporablja CDO (Collaboration Data Objects), poštno komponento COM, vgrajeno v operacijski sistem, podrobnost, ki dejansko pokvari večnitna paketna opravila, pa je inicializacija apartmaja COM in ne SMTP
Scenarij za tem API-jem je vsakdanji in zelo pogost: storitev izriše paket PDF-jev z izpiski ob koncu meseca, po enega za vsako stranko, nato pa jih mora poslati brez človeka v zanki. Če opravilo zaradi prepustnosti potisnete v bazen niti, se del pošiljanj začne končevati z napako COM, ki se pri izvajanju iste kode v eni niti nikoli ne ponovi. S strežnikom SMTP, PDF-jem ali priponko ni nič narobe. Težava je v tem, kaj CoInitializeEx vrne v niti, ki je CDO ni pričakoval, PDFlibPas pa je napisan tako, da ta primer načrtno obravnava in ne le po naključju
Kaj SendDocumentByMail dejansko počne v PDFlibPas
SendDocumentByMail je tanek orkestrator in ne samostojen poštni odjemalec. TPDFlib.SendDocumentByMail trenutno naložen dokument shrani v svoj začasni PDF, nastavitve SMTP in besedilo sporočila zapakira v zapis TPDFlibMailRequest, ta zapis preda vsemu, kar implementira IPDFlibMailProvider, nato pa začasno datoteko po vrnitvi ponudnika izbriše. Vmesnik ponudnika je dejanski poštni odjemalec, PDFlibPas pa vsebuje natanko eno vgrajeno izvedbo: ponudnika na osnovi CDO, ki se prevede samo v sistemu Windows. Če pokličete SendDocumentByMail brez predhodne nastavitve lastnosti MailProvider, PDFlibPas samodejno uporabi to privzeto izvedbo. Povratna vrednost je ves čas namenoma ozka: 1 pomeni sprejeto, 0 pa vse drugo, naj gre za manjkajoče zahtevano polje, napako pri pisanju začasne datoteke ali zavrnitev sporočila s strani ponudnika, dejanski razlog pa je nato na voljo samo prek GetLastMailError
var
PDF: TPDFlib;
Sent: Integer;
begin
PDF := TPDFlib.Create; // a new instance already holds one blank document
try
PDF.SetPageDimensions(612, 792); // US Letter, in points
PDF.NewPage;
// ... draw the statement: fonts, text, totals ...
Sent := PDF.SendDocumentByMail(
'smtp.example.com', 0, 1, // port 0 with SSL 1 falls back to 465
'billing@example.com', 'app-password', // SMTP auth
'billing@example.com', 'customer@example.com', '', '',
'Your statement is ready',
'Please find the attached PDF statement.',
'statement-4471.pdf'); // attachment display name
if Sent <> 1 then
Writeln('Send failed: ', PDF.GetLastMailError);
finally
PDF.Free;
end;
end;
Zakaj CoInitializeEx vrne S_FALSE in ali je to napaka
S_FALSE iz CoInitializeEx ni napaka, koda, ki ga obravnava kot napako, pa poroča o neuspehu v nitih, kjer se v resnici ni zgodilo nič slabega. CoInitializeEx vrne S_OK ob prvi uspešni inicializaciji COM v niti, S_FALSE pa, ko je bil COM v tej niti že inicializiran z združljivim modelom sočasnosti; v obeh primerih se isti referenčni števec za nit poveča, zato oba izida zahtevata ujemajoči se klic CoUninitialize, preden se nit konča ali nadaljuje z nepovezanim delom. Tudi TPDFlib sledi natanko temu vzorcu: ustvarjanje primerka TPDFlib že pokliče CoInitialize in zabeleži, ali dolguje ujemajoči se CoUninitialize, z enakim preverjanjem S_OK ali S_FALSE. Ko SendDocumentByMail doseže ponudnika CDO in ta znova pokliče CoInitializeEx, je COM v običajnem primeru v niti že inicializiran, zato ponudnik skoraj vedno vidi S_FALSE namesto S_OK. Obravnavati S_FALSE kot karkoli drugega kot uspeh v tej knjižnici ni redek robni primer, temveč običajna pot
InitResult := CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
NeedUninitialize := (InitResult = S_OK) or (InitResult = S_FALSE);
if Failed(InitResult) and (InitResult <> RPC_E_CHANGED_MODE) then
begin
ErrorText := 'COM initialization failed';
Exit;
end;
try
// ... create CDO.Message, CDO.Configuration, send ...
finally
if NeedUninitialize then
CoUninitialize;
end;
Zakaj CoInitializeEx vrne RPC_E_CHANGED_MODE
RPC_E_CHANGED_MODE pomeni, da je trenutna nit COM že prej inicializirala z drugačnim modelom sočasnosti od tistega, ki ga zahteva ta klic, običajno zato, ker je nit prej prešla v večnitni način (MTA), CDO pa zdaj prek COINIT_APARTMENTTHREADED zahteva semantiko enonitnega apartmaja (STA). Nit svoj model apartmaja izbere enkrat in tega modela do konca svojega obstoja ni mogoče spremeniti; ponovni klic CoInitializeEx z drugačnimi zastavicami neusklajenosti ne odpravi, klic CoUninitialize pred tem pa bi razgradil apartma, od katerega je lahko odvisna druga koda v tej niti. PDFlibPas obravnava RPC_E_CHANGED_MODE kot stanje, s katerim je treba delati, ne kot napako, o kateri bi bilo treba poročati: preskoči pripadajoči CoUninitialize, ker klic dejansko ni pridobil reference, ki bi jo bilo treba sprostiti, in omogoči nadaljevanje pošiljanja v obstoječem apartmaju
RPC_E_CHANGED_MODE se pojavlja skoraj izključno v ponovno uporabljenih nitih: pri delavcu bazena niti, niti IIS ali gostitelja storitve oziroma kateri koli niti, kjer je prejšnja koda, kot sta ADO ali WMI, že poklicala CoInitializeEx z COINIT_MULTITHREADED, še preden je poštna koda prišla blizu nje. Povsem nova nit, ki ne naredi ničesar drugega kot pokliče SendDocumentByMail, na to pot ne bo naletela. Delavska nit, ki jo razporejevalnik paketnih opravil vsak dan tisočkrat znova uporabi in si jo deli z drugim delom, ki temelji na COM, pa bo zagotovo, in to občasno, kar je natanko vzorec, zaradi katerega ljudje najprej preverijo strežnik SMTP in šele nato model niti
Kako preprečiti napačen imenik e-poštne priponke
PDFlibPas vsako odhodno priponko zapiše v svež imenik, poimenovan po GUID-u, ki ga ustvari ob vsakem klicu SendDocumentByMail, prav zato, da se sočasna pošiljanja nikoli ne morejo zaleteti v isto ime datoteke in da ime priponke ne more uiti iz tega imenika. Ime, posredovano kot priponka, se ne obravnava kot zaupanja vredna pot: gre skozi PLSanitizeAttachmentName, ki odstrani vsebino imenika, zavrne prazen niz ter posebni imeni . in .. in vsak znak, ki ga Windows obravnava kot nedovoljenega v imenu datoteke, skupaj z vsakim kontrolnim znakom, zamenja s podčrtajem. Če mu posredujete ..\quarter:report.pdf, del za prehajanje imenikov in del z nedovoljenim dvopičjem, bo na disk prišlo quarter_report.pdf: vse do zadnjega ločila poti se zavrže, dvopičje pa postane podčrtaj, ker se v imenu datoteke Windows ne sme pojaviti
function PLSanitizeAttachmentName(const FileName: WideString): WideString;
var
I, P: Integer;
begin
P := LastDelimiter('/\', string(FileName));
Result := Copy(FileName, P + 1, MaxInt); // strip any directory part
if (Result = '') or (Result = '.') or (Result = '..') then
Result := 'document.pdf';
for I := 1 to Length(Result) do
if (Ord(Result[I]) < 32) or (Pos(Result[I], WideString('<>:"/\|?*')) > 0) then
Result[I] := '_';
end;
Namenski imenik za posamezen klic ni zgolj stvar urejenosti. SendDocumentByMail po poslanem sporočilu v bloku finally izbriše začasno datoteko in odstrani njen imenik ter pri tem uporabi isto pot, v katero je zapisoval, zato bi ime priponke, ki bi do te kode prišlo brez čiščenja, povzročilo več kot le napačno mesto zapisa. Ista neočiščena pot bi nato prišla do koraka čiščenja, ki brez dodatnih preverjanj pokliče DeleteFile, v skupni začasni mapi pa bi lahko dve sočasni pošiljanji tudi tiho prepisali priponki druga druge z istim imenom, še preden bi bilo katero od njiju dostavljeno. Čiščenje imena zapre možnost prehajanja imenikov, imenik z GUID-om za posamezen klic pa možnost trčenja, pri čemer samo eno ali drugo ne bi zadostovalo
Ujemanje življenjske dobe COM z življenjsko dobo niti v delovnem bazenu
Najzanesljivejša rešitev za napake apartmajskega večnitnega izvajanja v paketnem poštnem programu je, da vsak klic SendDocumentByMail nehate obravnavati kot lastno izolirano življenjsko dobo COM in namesto tega COM inicializirate enkrat na delavsko nit, za celotno življenjsko dobo te niti. Delavec, ki ob zagonu pokliče CoInitializeEx(nil, COINIT_APARTMENTTHREADED), ta apartma ohrani za vsak svoj klic SendDocumentByMail in ob izhodu natanko enkrat pokliče CoUninitialize, pri lastnih pošiljanjih pošte nikoli ne bo videl RPC_E_CHANGED_MODE, ker nič drugega v tej niti ne bo dobilo priložnosti, da bi COM najprej inicializiralo v nasprotnem načinu. Vsak posamezen klic SendDocumentByMail po tem vzorcu znotraj sebe še vedno izvede svoj par CoInitializeEx in CoUninitialize, vendar je to neškodljivo: ker je delavska nit apartma že vzpostavila, vsak od teh notranjih klicev zdaj vidi S_FALSE, poveča in zmanjša isti referenčni števec ter pusti lastni COM-apartma delavske niti nedotaknjen
type
TMailWorker = class(TThread)
protected
procedure Execute; override;
end;
procedure TMailWorker.Execute;
var
PDF: TPDFlib;
Job: TStatementJob;
begin
CoInitializeEx(nil, COINIT_APARTMENTTHREADED);
try
while not Terminated do
begin
if not TryGetNextJob(Job) then
Break;
PDF := TPDFlib.Create;
try
BuildStatement(PDF, Job);
if PDF.SendDocumentByMail(Job.Host, 0, 1, Job.User, Job.Pass,
Job.From, Job.Recipient, '', '', Job.Subject, Job.Body,
Job.AttachmentName) <> 1 then
LogFailure(Job, PDF.GetLastMailError);
finally
PDF.Free;
end;
end;
finally
CoUninitialize;
end;
end;
Diagnosticiranje napak in testiranje brez pravega poštnega predala
GetLastMailError je druga polovica tega API-ja, ki jo je vredno že od začetka vključiti v beleženje, saj sama povratna vrednost 1 ali 0 ne pove, ali je neuspešno pošiljanje povzročila težava pri inicializaciji COM, zavrnitev preverjanja pristnosti SMTP ali manjkajoča priponka. Lastnost MailProvider omogoča testiranje celotne poti brez pravega poštnega predala: dodelite ji izvedbo IPDFlibMailProvider, ki zahteve beleži, namesto da jih pošilja, zaženite paketno opravilo s tem lažnim ponudnikom v cevovodu CI in ista mesta klicev SendDocumentByMail bodo še naprej delovala nespremenjeno, ko MailProvider pustite nenastavljeno in PDFlibPas v produkciji samodejno uporabi vgrajeni prenos CDO
Paketno opravilo, ki pošilja izpiske po e-pošti, se le redko konča pri pošiljanju: isti cevovod mora PDF pogosto še preveriti in podpisati, kar je ločeno obravnavano v članku o delovnem okolju za skladnost in podpisovanje, saj sta predhodno preverjanje in preverjanje podpisa drugačna vidika od dostave pošte, tudi kadar se izvajata drug za drugim. Če so poslani dokumenti rezultat velikega opravila združevanja ali razdeljevanja in ne enega sveže ustvarjenega PDF-ja, je ta korak ustvarjanja opisan v vodniku za neposreden dostop do velikih PDF-jev. SendDocumentByMail in tukaj opisani model ponudnika pošte sta del standardne PDFlibPas PDF Developer Library za Delphi in C++Builder, stran izdelka pa poleg preskusnega prenosa vsebuje tudi celoten API-referenčni opis