PDFlibPas, losLabova PDF razvojna biblioteka za Delphi i C++Builder, šalje generirani PDF kao privitak e-pošte jednim izravnim API pozivom, SendDocumentByMail. Na sustavu Windows zadani prijenos koristi CDO (Collaboration Data Objects), COM komponentu za poštu ugrađenu u operacijski sustav, a detalj koji doista ruši skupne poslove s više niti jest inicijalizacija COM apartmenta, a ne SMTP
Scenarij iza ovog API-ja nije glamurozan, ali je iznimno čest: servis generira skup PDF-ova s mjesečnim izvodima, po jedan za svakog korisnika, i svaki mora poslati bez ručne intervencije. Ako taj posao zbog bolje propusnosti prebacite u bazen niti, dio slanja počinje padati uz COM pogrešku koja se nikada ne pojavljuje kada isti kod radi u jednoj niti. Nije problem u SMTP poslužitelju, PDF-u ni privitku. Problem je ono što CoInitializeEx vraća u niti čija očekivanja CDO nije predvidio, a PDFlibPas je namjerno napisan tako da se s tim slučajem nosi
Što SendDocumentByMail zapravo radi unutar PDFlibPasa
SendDocumentByMail je tanki orkestrator, a ne samostalan klijent e-pošte. TPDFlib.SendDocumentByMail sprema trenutačno učitani dokument u vlastiti privremeni PDF, pakira SMTP postavke i tekst poruke u zapis TPDFlibMailRequest, predaje taj zapis onome što implementira IPDFlibMailProvider i ponovno briše privremenu datoteku kada se pružatelj vrati. Sučelje pružatelja stvarni je klijent e-pošte, a PDFlibPas isporučuje točno jednu ugrađenu implementaciju: pružatelja temeljenog na CDO-u koji se kompilira samo na sustavu Windows. Pozovete li SendDocumentByMail bez prethodne dodjele svojstva MailProvider, PDFlibPas se automatski vraća na taj zadani pružatelj. Povratna vrijednost namjerno ostaje uska: 1 znači prihvaćeno, a 0 sve ostalo, bilo da je riječ o nedostajućem obveznom polju, neuspjehu zapisivanja privremene datoteke ili odbijanju poruke od pružatelja, dok je stvarni razlog dostupan samo naknadno putem 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;
Zašto CoInitializeEx vraća S_FALSE i je li to neuspjeh?
S_FALSE koji vraća CoInitializeEx nije neuspjeh, a kod koji ga tako tretira prijavljuje neuspjehe u nitima u kojima zapravo ništa nije pošlo po zlu. CoInitializeEx vraća S_OK kada nit prvi put uspješno inicijalizira COM, a S_FALSE kada je COM u toj niti već inicijaliziran kompatibilnim modelom konkurentnosti, pri čemu u oba slučaja povećava isti brojač referenci po niti, pa oba ishoda zahtijevaju odgovarajući poziv CoUninitialize prije završetka niti ili prelaska na nepovezan posao. I sam TPDFlib slijedi upravo taj obrazac: konstrukcija instance TPDFlib već poziva CoInitialize i bilježi duguje li odgovarajući CoUninitialize, koristeći istu provjeru S_OK ili S_FALSE. Kada SendDocumentByMail zatim dođe do CDO pružatelja koji ponovno pozove CoInitializeEx, COM je u uobičajenom slučaju već inicijaliziran u toj niti, pa pružatelj gotovo uvijek vidi S_FALSE umjesto S_OK. Tretirati S_FALSE kao bilo što osim uspjeha nije rijedak rubni slučaj u ovoj biblioteci, nego uobičajeni put
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;
Zašto CoInitializeEx vraća RPC_E_CHANGED_MODE?
RPC_E_CHANGED_MODE znači da je trenutačna nit ranije inicijalizirala COM prema drugačijem modelu konkurentnosti od onoga koji ovaj poziv traži, obično zato što je nit prije radila u višestrukom modelu (MTA), a CDO sada traži semantiku jednonitnog apartmenta (STA) putem COINIT_APARTMENTTHREADED. Nit odabire svoj apartment model jednom i ništa taj model ne može promijeniti do kraja njezina života; ponovni poziv CoInitializeEx s drugim zastavicama ne popravlja neusklađenost, a prethodni poziv CoUninitialize srušio bi apartment o kojem drugi kod na toj niti još može ovisiti. PDFlibPas tretira RPC_E_CHANGED_MODE kao stanje s kojim treba raditi, a ne kao pogrešku koju treba prijaviti: preskače upareni CoUninitialize jer poziv nije stvarno preuzeo referencu koju treba osloboditi, pa slanje nastavlja u postojećem apartmentu
RPC_E_CHANGED_MODE pojavljuje se gotovo isključivo na ponovno korištenim nitima: radniku bazena niti, niti IIS-a ili servisnog domaćina, ili bilo kojoj niti u kojoj je raniji kod poput ADO-a ili WMI-ja već pozvao CoInitializeEx s COINIT_MULTITHREADED prije nego što je kôd za poštu došao do njega. Nova nit koja samo pozove SendDocumentByMail neće doći do ove grane. Radna nit koju planer skupnih poslova reciklira tisućama puta dnevno i dijeli s drugim COM poslovima hoće, i to povremeno, upravo po obrascu zbog kojega se najprije provjerava SMTP poslužitelj, a tek onda model rada s nitima
Kako zadržati privitak e-pošte izvan pogrešnog direktorija
PDFlibPas svaki odlazni privitak zapisuje u novi direktorij nazvan GUID-om koji generira pri svakom pozivu SendDocumentByMail, upravo zato da se istodobna slanja nikada ne sudare oko istog naziva datoteke i da naziv privitka ne može izaći iz tog direktorija. Naziv predan kao privitak ne tretira se kao putanja: prolazi kroz PLSanitizeAttachmentName, koji uklanja svaku komponentu direktorija, odbacuje prazan niz i posebna imena . i .., te svaki znak koji Windows smatra nedopuštenim u nazivu datoteke, kao i svaki kontrolni znak, zamjenjuje podvlakom. Ako mu predate ..\quarter:report.pdf, dio prolaska kroz direktorij i dio s nedopuštenom dvotočkom, na disk stiže quarter_report.pdf: sve do posljednjeg razdjelnika putanje odbacuje se, a dvotočka postaje podvlaka jer se ne smije pojaviti u nazivu datoteke sustava Windows
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;
Direktorij namijenjen svakom pozivu nije samo stvar urednosti. SendDocumentByMail briše privremenu datoteku i uklanja njezin direktorij u bloku finally nakon slanja poruke, koristeći isti put kojim je zapisivao, pa naziv privitka koji bi do tog koda stigao bez čišćenja ne bi samo premjestio zapis. Ista neprovjerena putanja zatim bi stigla do koraka čišćenja koji poziva DeleteFile bez dodatnih pitanja, a u zajedničkoj privremenoj mapi dva bi istodobna slanja mogla i tiho prebrisati privitak jedan drugome pod istim nazivom prije nego što bilo koja isporuka završi. Čišćenje naziva zatvara slučaj prolaska kroz direktorij, a zasebni direktorij s GUID-om slučaj sudara, i nijedno samo za sebe ne bi bilo dovoljno
Usklađivanje životnog vijeka COM-a sa životnim vijekom radne niti u bazenu niti
Najpouzdaniji popravak neuspjeha apartmenta u skupnom slanju pošte jest prestati svaki poziv SendDocumentByMail tretirati kao zaseban COM životni vijek i umjesto toga inicijalizirati COM jednom po radnoj niti, za cijelo trajanje te niti. Radnik koji pri pokretanju pozove CoInitializeEx(nil, COINIT_APARTMENTTHREADED), zadrži taj apartment za svaki svoj poziv SendDocumentByMail i pozove CoUninitialize točno jednom pri izlasku nikada neće od vlastitih slanja dobiti RPC_E_CHANGED_MODE, jer ništa drugo na toj niti neće stići prvo inicijalizirati COM u proturječnom modelu. Svaki pojedinačni poziv SendDocumentByMail i dalje interno izvršava vlastiti par CoInitializeEx i CoUninitialize, a to je bezopasno: budući da je apartment već uspostavila radna nit, svaki od tih unutarnjih poziva sada vidi S_FALSE, povećava i smanjuje isti brojač referenci te ostavlja vlastiti COM apartment radne niti netaknutim
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;
Dijagnosticiranje neuspjeha i testiranje bez aktivnog poštanskog sandučića
GetLastMailError druga je polovica ovog API-ja koju vrijedi od prvog dana uključiti u zapisivanje, jer sama povratna vrijednost 1 ili 0 ne govori je li neuspješno slanje uzrokovala inicijalizacija COM-a, odbijena SMTP provjera autentičnosti ili nedostajući privitak. Svojstvo MailProvider čini cijeli put provjerljivim bez pravog sandučića: dodijelite mu implementaciju IPDFlibMailProvider koja bilježi zahtjeve umjesto slanja, pokrenite skupni posao protiv lažnog pružatelja u CI cjevovodu i isti pozivi SendDocumentByMail ostaju nepromijenjeni kada u produkciji svojstvo MailProvider ostavite nedodijeljeno, pa se PDFlibPas vrati na ugrađeni CDO prijenos
Skupni posao koji šalje izvode e-poštom rijetko se zaustavlja na slanju: isti cjevovod često mora provjeriti i potpisati PDF prije slanja, što je zasebno obrađeno u članku o radnom stolu za provjeru usklađenosti i potpisivanje, jer su prethodna provjera i potvrda potpisa drugačija briga od isporuke pošte čak i kada se odvijaju jedna za drugom. Kada su dokumenti koji se šalju rezultat velikog posla spajanja ili razdvajanja, a ne jednog svježe izrađenog PDF-a, vodič za izravan pristup velikim PDF-ovima pokriva taj korak generiranja. SendDocumentByMail i ovdje opisani model pružatelja dio su standardne PDFlibPas PDF razvojne biblioteke za Delphi i C++Builder, a stranica proizvoda uz probno preuzimanje sadržava i potpuni API priručnik