PDFlibPas, losLab PDF Developer Library for Delphi og C++Builder, sender en generert PDF som et e-postvedlegg gjennom ett flatt API-kall, SendDocumentByMail. På Windows bruker standardtransporten CDO (Collaboration Data Objects), COM-e-post-komponenten innebygd i operativsystemet, og detaljen som faktisk ødelegger multi-trådede batch-jobber, er COM-apartment-initialisering, ikke SMTP
Scenarioet bak dette API-et er lite glamorøst og ekstremt vanlig: en tjeneste genererer en batch med månedsslutt-kontoutskrift-PDF-er, én per kunde, og må sende hver ut per e-post uten en person i løkken. Skyv den jobben over på en trådpool for gjennomstrømming, og en brøkdel av utsendelsene begynner å feile med en COM-feil som aldri reproduserer når den samme koden kjører på én enkelt tråd. Ingenting er galt med SMTP-serveren, PDF-en, eller vedlegget. Problemet er hva CoInitializeEx returnerer på en tråd CDO ikke forventet, og PDFlibPas er skrevet for å håndtere det tilfellet bevisst snarere enn ved en tilfeldighet
Hva SendDocumentByMail faktisk gjør inne i PDFlibPas
SendDocumentByMail er en tynn orkestrator, ikke en e-postklient i sin egen rett. TPDFlib.SendDocumentByMail lagrer det gjeldende innlastede dokumentet til sin egen midlertidige PDF, pakker SMTP-innstillingene og meldingsteksten inn i en TPDFlibMailRequest-record, overleverer den recorden til hva som enn implementerer IPDFlibMailProvider, og sletter den midlertidige filen igjen når leverandøren returnerer. Leverandørgrensesnittet er den faktiske e-postklienten, og PDFlibPas leverer nøyaktig én innebygd implementasjon: en CDO-basert leverandør som bare kompilerer på Windows. Kall SendDocumentByMail uten å tildele MailProvider-egenskapen først, og PDFlibPas faller automatisk tilbake til den standarden. Returverdien forblir bevisst snever gjennomgående: 1 for akseptert, 0 for alt annet, enten det er et manglende obligatorisk felt, en midlertidig-fil-skrivefeil, eller at leverandøren avviser meldingen, med den faktiske grunnen tilgjengelig bare fra GetLastMailError i etterkant
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;
Hvorfor returnerer CoInitializeEx S_FALSE, og er det en feil?
S_FALSE fra CoInitializeEx er ikke en feil, og kode som behandler den som en, rapporterer feil på tråder der ingenting faktisk gikk galt. CoInitializeEx returnerer S_OK første gang en tråd vellykket initialiserer COM, og den returnerer S_FALSE når den tråden allerede hadde COM initialisert med en kompatibel samtidighetsmodell, og øker den samme per-tråd-referansetellingen uansett, så begge utfallene trenger et tilhørende CoUninitialize-kall før tråden avslutter eller går videre til urelatert arbeid. TPDFlib selv følger nøyaktig dette mønsteret: å konstruere en TPDFlib-instans kaller allerede CoInitialize og registrerer om en tilhørende CoUninitialize skyldes, ved bruk av den identiske S_OK-eller-S_FALSE-sjekken. Innen SendDocumentByMail når sin CDO-leverandør og den leverandøren kaller CoInitializeEx igjen, er COM derfor allerede initialisert på tråden i det vanlige tilfellet, så leverandøren observerer nesten alltid S_FALSE snarere enn S_OK. Å behandle S_FALSE som noe annet enn suksess er ikke et sjeldent grensetilfelle i dette biblioteket; det er den vanlige veien
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;
Hvorfor returnerer CoInitializeEx RPC_E_CHANGED_MODE?
RPC_E_CHANGED_MODE betyr at den gjeldende tråden initialiserte COM tidligere under en annen samtidighetsmodell enn den dette kallet forespør, typisk fordi tråden tidligere gikk multi-tråded (MTA) og CDO nå ber om enkelt-tråded-apartment (STA)-semantikk gjennom COINIT_APARTMENTTHREADED. En tråd velger sin apartment-modell én gang, og ingenting kan endre den modellen resten av trådens levetid; å prøve CoInitializeEx på nytt med forskjellige flagg fikser ikke mismatchen, og å kalle CoUninitialize først ville rive ned en apartment annen kode på den tråden fortsatt kan være avhengig av. PDFlibPas behandler RPC_E_CHANGED_MODE som en tilstand å jobbe med snarere enn en feil å rapportere: den hopper over det parede CoUninitialize-kallet, ettersom kallet aldri faktisk anskaffet en referanse å slippe, og lar utsendelsen fortsette på den eksisterende apartment-en
RPC_E_CHANGED_MODE dukker opp nesten utelukkende på gjenbrukte tråder: en trådpool-arbeider, en IIS- eller tjeneste-vert-tråd, eller enhver tråd der tidligere kode som ADO eller WMI allerede kalte CoInitializeEx med COINIT_MULTITHREADED før e-postkoden kom i nærheten av den. En helt ny tråd som ikke gjør noe annet enn å kalle SendDocumentByMail, vil ikke treffe denne veien. En arbeidertråd resirkulert tusenvis av ganger om dagen av en batch-planlegger, og delt med annet COM-basert arbeid, vil absolutt gjøre det, og den vil gjøre det periodisk, noe som er nøyaktig mønsteret som sender folk til å se på SMTP-serveren først og trådmodellen som nummer to
Å holde et e-postvedlegg unna feil katalog
PDFlibPas skriver hvert utgående vedlegg inn i en fersk katalog oppkalt etter en GUID den genererer ved hvert SendDocumentByMail-kall, spesifikt slik at samtidige utsendelser aldri kan kollidere på det samme filnavnet og slik at et vedleggsnavn ikke kan gå ut av den katalogen. Navnet sendt inn som vedlegget stoles ikke på som en sti: det går gjennom PLSanitizeAttachmentName, som fjerner enhver katalogkomponent, avviser den tomme strengen og de spesielle .- og ..-navnene, og erstatter hvert tegn Windows behandler som ulovlig i et filnavn, sammen med ethvert kontrolltegn, med en understrek. Mat den ..\quarter:report.pdf, delvis katalog-traversering og delvis ulovlig kolon, og hva som når disk, er quarter_report.pdf: alt opp til den siste sti-separatoren kastes, og kolonet blir en understrek fordi det ikke kan opptre i et Windows-filnavn
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;
En dedikert per-kall-katalog er ikke bare ryddighet. SendDocumentByMail sletter den midlertidige filen og fjerner katalogen dens i en finally-blokk etter at meldingen er sendt, ved bruk av nøyaktig den samme stien den skrev til, så et vedleggsnavn som nådde den koden usanert, ville ikke bare feilplassere skrivingen. Den samme usanerte stien ville da nå et opprydningstrinn som kaller DeleteFile uten å stille flere spørsmål, og på en delt midlertidig-mappe kunne to samtidige utsendelser også stille overskrive hverandres vedlegg under det samme navnet før noen av leveringene fullførte. Å sanere navnet lukker traverserings-tilfellet, og per-kall-GUID-katalogen lukker kollisjons-tilfellet, og ingen av dem alene ville vært nok
Å matche COM-levetid med tråd-levetid i en arbeiderpool
Den mest pålitelige løsningen for apartment-tråding-feil i en batch-e-post-utsender er å slutte å behandle hvert SendDocumentByMail-kall som sin egen isolerte COM-levetid, og i stedet initialisere COM én gang per arbeidertråd, for den trådens levetid. En arbeider som kaller CoInitializeEx(nil, COINIT_APARTMENTTHREADED) når den starter, beholder den apartment-en for hvert SendDocumentByMail-kall den gjør, og kaller CoUninitialize nøyaktig én gang når den avslutter, vil aldri se RPC_E_CHANGED_MODE fra sine egne e-post-utsendelser, fordi ingenting annet på den tråden får sjansen til å initialisere COM i en motstridende modus først. Hvert individuelle SendDocumentByMail-kall kjører fortsatt sitt eget CoInitializeEx- og CoUninitialize-par internt under dette mønsteret, og det er harmløst: med apartment-en allerede etablert av arbeidertråden, ser hvert eneste av de interne kallene nå S_FALSE, øker og senker den samme referansetellingen, og lar arbeidertrådens egen COM-apartment være urørt
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;
Å diagnostisere feil og teste uten en levende postboks
GetLastMailError er den andre halvparten av dette API-et verdt å bygge inn i logging fra dag én, fordi 1-eller-0-returverdien alene ikke sier om en mislykket utsendelse var et COM-initialiserings-problem, en SMTP-autentiseringsavvisning, eller et manglende vedlegg. MailProvider-egenskapen er det som gjør hele veien testbar uten en ekte postboks: tildel den en IPDFlibMailProvider-implementasjon som registrerer forespørsler i stedet for å sende dem, kjør en batch-jobb mot den falske leverandøren i en CI-pipeline, og de samme SendDocumentByMail-kallestedene fortsetter å fungere uendret når MailProvider lates usatt og PDFlibPas faller tilbake til den innebygde CDO-transporten i produksjon
En batch-jobb som sender kontoutskrifter per e-post, stopper sjelden ved sending: den samme pipelinen trenger ofte å validere og signere PDF-en før den går ut, noe som er dekket separat i konformitets- og signerings-arbeidsbenk-artikkelen, ettersom preflight og signaturverifisering er et annet anliggende enn e-postlevering selv når begge kjører etter hverandre. Når dokumentene som sendes per e-post, selv er utdataen fra en stor sammenslåings- eller delings-jobb i stedet for en enkelt ferskt bygget PDF, dekker stor-PDF-direkte-tilgang-guiden det genereringstrinnet. SendDocumentByMail og e-post-leverandør-modellen beskrevet her, er en del av standard PDFlibPas PDF Developer Library for Delphi og C++Builder, og produktsiden bærer den fulle API-referansen sammen med en prøvenedlasting