Indy. sending Attach pdf by stream - delphi

I'm having trouble sending attachments via stream.
I use Indy 10.6.2 and Delphi Berlin.
The mail consists of html with attached images, plus one or more PDF files inserted directly from the database.
I don't get any errors in the process.
Mail is sent seamlessly, but attached PDFs are not received.
I look forward to comments
Msg := TIdMessage.Create(self);
try
Msg.ContentType := 'multipart/mixed';
Msg.From.Name := FromName;
Msg.From.Address := FromAddress;
Msg.Priority := mpHigh;
Msg.Subject := Asunto;
with TIdMessageBuilderHtml.Create do
try
if FMsgTxtPlnPac <> '' then
PlainText.Text := FMsgTxtPlnPac;
if FMsgHtmlPac <> '' then
begin
Html.Text := FMsgHtmlPac;
n := 0;
for s in FMsgHTMLFiles.Split([',']) do
begin
n := Succ(n);
c := 'img' + inttostr(n);
HTMLFiles.Add(s, c); // cid de imagen en HTML(cid:img1, cid:img2...)
end;
end;
if AttachFiles <> '' then
for s in AttachFiles.Split([',']) do
Attachments.Add(s);
// Attach from DB
while not dm.qryBlb.Eof do
begin
Attach := TIdAttachmentMemory.Create(Msg.MessageParts);
Attach.ContentType := 'application/pdf';
Attach.FileName := dm.qryBlb.FieldByName('nombre_archivo').AsString;
Attach.LoadFromStream(dm.GetDataBlbStrm('DATA_TXT')); // this ok in attach.datastream.size
Attach.CloseLoadStream;
dm.qryBlb.Next;
end;
FillMessage(Msg);
finally
Free;
end;
for s in FMailPac.Split([',']) do
begin
EmailAddress := Trim(s);
if EmailAddress <> '' then
begin
with Msg.recipients.Add do
begin
Address := EmailAddress;
end;
end;
end;
for s in MailCC.Split([',']) do
begin
EmailAddress := Trim(s);
if EmailAddress <> '' then
Msg.CCList.Add.Address := EmailAddress;
end;
for s in MailCCO.Split([',']) do
begin
EmailAddress := Trim(s);
if EmailAddress <> '' then
Msg.BccList.Add.Address := EmailAddress;
end;
finally
SMTP1.Send(Msg);
end;

TIdMessageBuilderHtml supports adding attachments via streams, as well as via files. However, those streams have to remain alive for the duration that the TIdCustomMessageBuilder.Attachments collection is populated, which is not an option in your case since you are looping through DB records one at a time, thus you would only be able to access 1 DB stream at a time.
You could create a local array/list of TMemoryStream objects, and then populate the TMessageBuilderHtml with those streams, but you will end up wasting a lot of memory that way since TIdMessageBuilderHtml would make its own copy of the TMemoryStream data. And there is no way to have TIdMessageBuilderHtml just use your TMemoryStream data as-is in a read-only mode (hmm, I wonder if I should add that feature!).
The reason why your manual TIdAttachmentMemory objects don't work is simply because TIdCustomMessageBuilder.FillMessage() clears the TIdMessage's body before then re-populating it, thus losing your attachments (and various other properties that you are setting manually beforehand).
You would have to add your DB attachments to the TIdMessage after FillMessage() has done its work first. But, then you risk TIdMessageBuilderHtml not setting up the TIdMessage structure properly since it wouldn't know your DB attachments exist.
On a side note, you are not using TIdAttachmentMemory correctly anyway. Do not call its CloseLoadStream() method if you have not called its OpenLoadStream() method first. Calling its LoadFromStream() method is enough in this case (or, you can even pass the TStream to TIdAttachmentMemory's constructor). Do note, however, that you are leaking the TStream returned by dm.GetDataBlbStrm().
So, in this case, you are probably better off simply populating the TIdMessage manually and not use TIdMessageBuilderHtml at all. Or, you could derive a new class from TIdMessageBuilderHtml (or TIdCustomMessageBuilder directly) and override its virtual FillBody() and FillHeaders() methods to take your DB streams into account.

Related

Access violation error at delphi while saving/loading image to/from stream

I am developing an application in delphi. I am trying to extract an image that is saved in database, save it to TMemoryStream and load same image at TImage control placed on other form that will populate dynamically. I am getting access violation error when I try to load image from stream to image control placed on the form.
Error Description is as follows
Access violation at address 00B548C in module abc.exe. Read of address 0000000
My code snippet is as follows
UniConnection1.Connected := true;
UniQuery2.SQL.Text := 'Select image from userplays where id = :id';
UniQuery2.Params.ParamByName('id').Value := idpub1;
UniQuery2.Open;
if UniQuery2.FieldByName('image').AsString <> '' then
begin
try
Stream121 := TMemoryStream.Create;
TBlobField(UniQuery2.FieldByName('image')).SaveToStream(Stream121);
Stream121.Position := 0;
if Assigned(Stream121) then
begin
Image1.Picture.Graphic.LoadFromStream(Stream121);
Image1.Update;
end;
finally
Stream121.Free;
end;
end;
TPicture is not able to determine the graphic type in the stream, so you have to tell it before. If you have only JPEG images, you can just hardcode that. Otherwise you should store the image format in the database, too.
var
graphic: TGraphic;
Stream121.Position := 0;
if Stream121.size > 0 then begin
graphic := TJPEGImage.Create;
try
graphic.LoadFromStream(Stream121);
Image1.Picture.Graphic := graphic;
finally
graphic.Free;
end;
end;
You are referring to Graphic.LoadfromStream. But Graphic may not (probably will not) exist. You could save to a file and use Picture.LoadFromFile instead (as this will create the appropriate TGraphic descendant) or create Picture.Graphic as the appropriate type (eg TBitmap) first.
Picture.Graphic := TBitMap.Create;
As it stands the image has no idea of what graphic format your data is in. You will need to tell it somehow.

Retrieve Outlook logged-in user SMTP address after connecting through OLE

Exchange Web Services has a ResolveNames() function that I can use to retrieve (among other things) the primary SMTP address for the Active Directory user that logged on to Exchange Server through EWS.
I am now programming through OLE against Outlook and would like the same functionality.
I have been browsing through the Outlook object model but can't find an appropriate object or method.
Does anyone know of an object/method that I can use to get the primary SMTP address?
Below is the current Delphi code that I use to connect to Outlook.
For the default user logging in (AUserSMTP='') it returns the OutlookApp COM Object (through GetActiveOleObject or CreateOleObject), a NameSpace (through GetNameSpace) and a Folder (through GetDefaultFolder) object, but I could not find where to go from there.
I thought lNameSpace.CurrentUser (a Recipient object) might lead somewhere, but its Address property only returns a string like '/o=TimeTell/ou=Exchange Administrative Group (FYDIBOHF23SPDLT)/cn=Recipients/cn=developer' without email address...
Any suggestions about the route to take?
function TDataModuleSyncOutlook.ConnectToOutlook(AUserSMTP: String = ''): Boolean;
var
lNameSpace, lRecipient: OleVariant;
begin
Result := false;
FWasCreated := False;
try
FOutlookApp := GetActiveOleObject(scxOutlookApp);
Result := True;
except
try
FOutlookApp := CreateOleObject(scxOutlookApp);
FWasCreated := True;
Result := True;
except
on E:Exception do ...
end;
end;
if Result then
begin
lNameSpace := FOutlookApp.GetNameSpace(scxNameSpace);
if AUserSMTP <> '' then // This part not applicable to the question
begin // Open shared calendar als er een expliciete gebruiker is opgegeven...
lRecipient := lNameSpace.CreateRecipient(AUserSMTP);
try
FCalendarFolder := lNameSpace.GetSharedDefaultFolder(lRecipient, olFolderCalendar);
except
on E:Exception do ...
end;
end
else // ... anders de default calendar folder openen
FCalendarFolder := lNameSpace.GetDefaultFolder(olFolderCalendar);
end;
FOleInitialized := Result;
if Result then TSyncLogger.LogAlways('Connected to Outlook') else TSyncLogger.LogAlways('Connection to Outlook failed');
end;
Try to use Application.Session.CurrentUser.AddressEntry.GetExchangeUser.PrimarySmtpAddress (you would of course need to check for nulls).
As for the account order, you can either use Extended MAPI and IOlkAccountManager.GetOrder (you can play with that object in OutlookSpy (I am its author) if you click IOlkAccountManager button) or you can use Redemption (I am also its author) and its RDOSession.Accounts.GetOrder method (see http://www.dimastr.com/redemption/RDOAccounts.htm). The first account in the returned collection will be the default one.
I found it. I have to go through the Accounts object in the namespace:
for i := 1 to lNameSpace.Accounts.Count do
if lNameSpace.Accounts.Item[i].AccountType = olExchange then
begin
lAccount := lNameSpace.Accounts.Item[i];
Break;
end;
if VarIsClear(lAccount) then
begin
DisConnectFromOutlook;
Exit;
end;
lLoginSMTP := lAccount.SmtpAddress;
The only thing I would still like is to determine the default account.

Memo1.Lines.LoadFromStream from IOHandler.ReadStream

I was trying to display the sent textfile to memo.lines with out saving it to a disk
from server
try
Ms := TMemoryStream.Create;
Ms.LoadFromFile('update.txt');
Ms.Position := 0;
AContext.Connection.IOHandler.LargeStream := True;
AContext.Connection.IOHandler.Write(Ms, 0, True);
finally
Ms.Free;
end;
to client...im not sure how to do this in client
try
Ms := TMemoryStream.Create;
Ms.Position := 0;
IdTCPClient1.IOHandler.LargeStream := True;
IdTCPClient1.Connection.IOHandler.ReadStream(Ms, -1,false);
finally
Memo1.Lines.LoadFromStream(Ms);
Ms.Free;
end;
can anyone help me on how to make this work if its possible ?
Your code is fine, you simply forgot to reset the stream's Position property back to 0 before calling the Memo's LoadFromStream() method:
IdTCPClient1.Connection.IOHandler.ReadStream(Ms, -1,false);
Ms.Position := 0; // <-- add this
Memo1.Lines.LoadFromStream(Ms);
If I were you, I would still buffer the read data because of two reasons:
The network stream is unidirectional and non-positionable therefore not really file like. (Yes, it also means, that you must refresh it manually/programmatically every time some new data is received.)
If the connection fails the read buffer could be filled with gibberish which gets diplayed, because You have no further means to control what gets displayed once you redirected the input of your memo.
If you do not want to save the sent data to disk, you could still save it to a TMemoryStream instance, which could be used as a parameter of the memo's .LoadFromStream() method.

How to get body texts of all e-mail messages from a certain IMAP mailbox?

How to get body texts of all e-mail messages from a certain IMAP mailbox in Delphi ? For example, from the INBOX mailbox ?
There are many ways to retrieve all body texts of all messages from the selected mailbox. I've used the one, where you iterate the mailbox and Retrieve every single message from the mailbox one by one. This way allows you to modify the code, so that you'll be able to break the loop when you need or e.g. replace Retrieve by RetrievePeek which won't mark the message as read on server like the first mentioned does. When the message is retrieved from server, all its parts are iterated and when it's the text part, its body is appended to a local S variable. After the iteration the S variable is added to the output BodyTexts string list. So, as the result you'll get string list collection where each item consists from the concatenated message's text part bodies and where each item means one message.
uses
IdIMAP4, IdSSLOpenSSL, IdText, IdMessage, IdExplicitTLSClientServerBase;
procedure GetGmailBodyTextParts(const UserName, Password: string;
BodyTexts: TStrings);
var
S: string;
MsgIndex: Integer;
MsgObject: TIdMessage;
PartIndex: Integer;
IMAPClient: TIdIMAP4;
OpenSSLHandler: TIdSSLIOHandlerSocketOpenSSL;
begin
BodyTexts.Clear;
IMAPClient := TIdIMAP4.Create(nil);
try
OpenSSLHandler := TIdSSLIOHandlerSocketOpenSSL.Create(nil);
try
OpenSSLHandler.SSLOptions.Method := sslvSSLv3;
IMAPClient.IOHandler := OpenSSLHandler;
IMAPClient.Host := 'imap.gmail.com';
IMAPClient.Port := 993;
IMAPClient.UseTLS := utUseImplicitTLS;
IMAPClient.Username := UserName;
IMAPClient.Password := Password;
IMAPClient.Connect;
try
if IMAPClient.SelectMailBox('INBOX') then
begin
BodyTexts.BeginUpdate;
try
for MsgIndex := 1 to IMAPClient.MailBox.TotalMsgs do
begin
MsgObject := TIdMessage.Create(nil);
try
S := '';
IMAPClient.Retrieve(MsgIndex, MsgObject);
MsgObject.MessageParts.CountParts;
if MsgObject.MessageParts.TextPartCount > 0 then
begin
for PartIndex := 0 to MsgObject.MessageParts.Count - 1 do
if MsgObject.MessageParts[PartIndex] is TIdText then
S := S + TIdText(MsgObject.MessageParts[PartIndex]).Body.Text;
BodyTexts.Add(S);
end
else
BodyTexts.Add(MsgObject.Body.Text);
finally
MsgObject.Free;
end;
end;
finally
BodyTexts.EndUpdate;
end;
end;
finally
IMAPClient.Disconnect;
end;
finally
OpenSSLHandler.Free;
end;
finally
IMAPClient.Free;
end;
end;
This code requires OpenSSL, so don't forget to put the libeay32.dll and ssleay32.dll libraries to a path visible to your project; you can download OpenSSL libraries for Indy in different versions and platforms from here.

How to make Word invisible during OLE automation from Delphi

From our application we use OLE automation to build a fairly complex Word-document. I would like to make Word invisible while the document is being made, since there is a lot of pasting and insertions that takes quite a long time.
I use the following code to establish a Word connection:
function ConnectToWord : TWordAutomationResult;
begin
WordApp := TWordApplication.Create(nil);
try
WordApp.Connect;
WordApp.Visible := false;
except on E: Exception do
begin
Result := waeErrorConnectingToWord;
exit;
end;
end;
end;
And I use the following code to open an existing document, which is then edited by my application.
function TWordAUtomation.OpenDocument(aFileName: string) : WordDocument;
var vFileName,
vConfirmConversions,
vReadOnly,
vAddToRecentFiles,
vPasswordDocument,
vPasswordTemplate,
vRevert,
vWritePasswordDocument,
vWritePasswordTemplate,
vFormat,
vEncoding,
vVisible,
vOpenConflictDocument,
vOpenAndRepair,
vWdDocumentDirection,
vNoEncodingDialog : OleVariant;
begin
Result := nil;
if not FileExists(aFileName) then exit;
vFileName := aFileName;
vConfirmConversions := True;
vReadOnly := False;
vAddToRecentFiles := False;
vPasswordDocument := EmptyParam;
vPasswordTemplate := EmptyParam;
vRevert := True;
vWritePasswordDocument := EmptyParam;
vWritePasswordTemplate := EmptyParam;
vFormat := wdOpenFormatAuto;
vEncoding := EmptyParam;
vVisible := False; //Document should be invisible
vOpenConflictDocument := EmptyParam;
vOpenAndRepair := EmptyParam;
vWdDocumentDirection := EmptyParam;
vNoEncodingDialog := EmptyParam;
Result := WordApp.Documents.Open(vFileName, vConfirmConversions, vReadOnly, vAddToRecentFiles, vPasswordDocument, vPasswordTemplate, vRevert, vWritePasswordDocument, vWritePasswordTemplate, vFormat, vEncoding, vVisible, vOpenAndRepair, vWdDocumentDirection, vNoEncodingDialog);
end;
It works on my computer! (TM)
For some of our customers Word remains visible during the editing process. What reasons can there be for this? As far as I can tell the problem arises for customers that use some sort of remote computing, like managed clients etc. Are there some additional properties that deals with application visibility that only have effect during remote desktop connections etc? I'm not very knowledgeable about such things :-(
I'm maintaining the Word automation for our software and also had reports of Word windows popping up in Citrix clients. I don't know what causes this and how to get rid of it.
There is only one way I can simulate Word becoming visible again and that is opening a Word-document while your application is processing. But I don't think that is the cause of your problems.
PS: You call TWordApplication.Connect and then you set Visible to False. Know that when you call Connect and you haven't changed ConnectKind, it will connect to a running instance of Word. When your client is editing a document this document will suddenly dissappear. Perhaps it is better to set ConnectKind to NewInstance so you always work in a new winword.exe process. The existing winword.exe will remain available for your client and he can continue working at his document while your application is processing the other.
Ofcourse this approach has some drawbacks too:
When your client opens a new Word-document, it is opened in your instance of Word
You can get errors on Normal.dot being modified by another application
Instead of using TWordApplication, use CreateOLEObject:
var WordApp: Variant;
procedure OpenWordFIle( const Filename: String );
begin
WordApp := CreateOLEObject('Word.Application');
WordApp.Visible := False;
WordApp.Documents.Open( Filename );
Application.ProcessMessages;
end;
To close it gracefully:
procedure CloseWordFile;
begin
WordApp.ActiveDocument.Close( $00000000 );
WordApp.Quit;
WordApp := unassigned;
end;
If you don't close it, Word application will be open even after your close your Delphi application.
Some useful resources where you can find more options to open/close Word Files:
http://msdn.microsoft.com/en-us/library/office/ff835182.aspx
How can I call documents.open and avoid the 'file in use' dialog?
in my case it happend similar as you described. I looks the application is still running even if you disconnect. The first time it will not be shown, but as soon as you have a second open then the application will be visible. in My case it helped to explicitly quite the application. It quit's only the instance that is doing the work in background. Another open document edited by the local user will not be touched.
WordDocument.Disconnect;
**WordApplication.Quit;**
WordApplication.Disconnect;

Resources