I have been playing around with some code to send mail using MAPI in my Delphi XE7 program.
I have managed to get the To, Cc and Bcc working together with fileattachment, but when the mail is send by the client (in this case Outlook 2010) the program crashes and I simply can't figure out where I am am doing something wrong - I might be staring blind on the code.
I have made a small example with the code in a file in my dropbox
dl.dropboxusercontent.com/u/65392149/Mapi_MCVE.ZIP
The MAPI function looks like this
function SendMailMAPI(const aFrom, aTo, aCc, aBcc, aSubject, aBody: string; aMailFiles: TStringList; aReceipt: boolean): boolean;
var
MapiStatus: DWord;
MapiMessage: TMapiMessage;
MapiOrigin: TMapiRecipDesc;
MapiRecipient: array of TMapiRecipDesc;
MapiFiles: PMapiFileDesc;
RecipientsTo: TStringlist;
RecipientsCc: TStringlist;
RecipientsBcc: TStringlist;
RecipientsCount: integer;
FilesCount: Integer;
i: integer;
Filename: string;
begin
MapiStatus := SUCCESS_SUCCESS;
Result := True;
MapiFiles := nil;
FillChar(MapiMessage, Sizeof(TMapiMessage), 0);
if aReceipt then
MapiMessage.flFlags := MAPI_RECEIPT_REQUESTED;
MapiMessage.lpszSubject := PAnsiChar(AnsiString(aSubject));
MapiMessage.lpszNoteText := PAnsiChar(AnsiString(aBody));
FillChar(MapiOrigin, Sizeof(TMapiRecipDesc), 0);
MapiOrigin.lpszName := PAnsiChar(AnsiString(aFrom));
MapiOrigin.lpszAddress := PAnsiChar(AnsiString(aFrom));
MapiMessage.lpOriginator := nil;
FilesCount := aMailFiles.Count;
if FilesCount > 0 then
begin
GetMem(MapiFiles, SizeOf(TMapiFileDesc) * FilesCount);
for i := 0 to FilesCount - 1 do
begin
FileName := aMailfiles[i];
MapiFiles[i].ulReserved := 0;
MapiFiles[i].flFlags := 0;
MapiFiles[i].nPosition := ULONG($FFFFFFFF);
MapiFiles[i].lpszPathName := PAnsiChar(AnsiString(FileName));
MapiFiles[i].lpszFileName := PAnsiChar(AnsiString(ExtractFileName(FileName)));
MapiFiles[i].lpFileType := nil;
end;
MapiMessage.nFileCount := FilesCount;
MapiMessage.lpFiles := #MapiFiles^;
end;
RecipientsCount := 0;
RecipientsTo := TStringlist.Create;
RecipientsCc := TStringlist.Create;
RecipientsBcc := TStringlist.Create;
RecipientsTo.Delimiter := ';';
RecipientsCc.Delimiter := ';';
RecipientsBcc.Delimiter := ';';
try
if aTo <> '' then
begin
RecipientsTo.DelimitedText := aTo;
RecipientsCount := RecipientsCount + RecipientsTo.Count;
end;
if aCc <> '' then
begin
RecipientsCc.DelimitedText := aCc;
RecipientsCount := RecipientsCount + RecipientsCc.Count;
end;
if aBcc <> '' then
begin
RecipientsBcc.DelimitedText := aBcc;
RecipientsCount := RecipientsCount + RecipientsBcc.Count;
end;
FillChar(MapiRecipient, Sizeof(TMapiRecipDesc) * RecipientsCount, 0);
SetLength(MapiRecipient, RecipientsCount);
RecipientsCount := 0;
if RecipientsTo.Count > 0 then
begin
MapiRecipient[RecipientsCount].ulRecipClass := MAPI_TO;
for i := 0 to RecipientsTo.Count - 1 do
begin
MapiRecipient[RecipientsCount].lpszName := PAnsiChar(AnsiString(RecipientsTo[i]));
MapiRecipient[RecipientsCount].lpszAddress := PAnsiChar(AnsiString(RecipientsTo[i]));
Inc(RecipientsCount);
end;
end;
if RecipientsCc.Count > 0 then
begin
MapiRecipient[RecipientsCount].ulRecipClass := MAPI_CC;
for i := 0 to RecipientsCc.Count - 1 do
begin
MapiRecipient[RecipientsCount].lpszName := PAnsiChar(AnsiString(RecipientsCc[i]));
MapiRecipient[RecipientsCount].lpszAddress := PAnsiChar(AnsiString(RecipientsCc[i]));
Inc(RecipientsCount);
end;
end;
if RecipientsBcc.Count > 0 then
begin
MapiRecipient[RecipientsCount].ulRecipClass := MAPI_BCC;
for i := 0 to RecipientsBcc.Count - 1 do
begin
MapiRecipient[RecipientsCount].lpszName := PAnsiChar(AnsiString(RecipientsBcc[i]));
MapiRecipient[RecipientsCount].lpszAddress := PAnsiChar(AnsiString(RecipientsBcc[i]));
Inc(RecipientsCount);
end;
end;
MapiMessage.nRecipCount := RecipientsCount;
MapiMessage.lpRecips:= Pointer(MapiRecipient);
finally
RecipientsTo.Free;
RecipientsCc.Free;
RecipientsBcc.Free;
end;
try
MapiStatus := MapiSendMail(0, Application.MainForm.Handle, MapiMessage, MAPI_LOGON_UI + MAPI_DIALOG, 0);
except
on E:Exception do
ShowMessage('U_Mailing.Mapi.SendMailMAPI: ' + E.Message);
end;
for i := 0 to FilesCount - 1 do
begin
System.AnsiStrings.StrDispose(MapiFiles[i].lpszPathName);
System.AnsiStrings.StrDispose(MapiFiles[i].lpszFileName);
end;
for i := 0 to RecipientsCount - 1 do
begin
System.AnsiStrings.StrDispose(MapiRecipient[i].lpszName);
System.AnsiStrings.StrDispose(MapiRecipient[i].lpszAddress);
end;
case MapiStatus of
MAPI_E_AMBIGUOUS_RECIPIENT:
Showmessage('A recipient matched more than one of the recipient descriptor structures and MAPI_DIALOG was not set. No message was sent.');
MAPI_E_ATTACHMENT_NOT_FOUND:
Showmessage('The specified attachment was not found; no message was sent.');
MAPI_E_ATTACHMENT_OPEN_FAILURE:
Showmessage('The specified attachment could not be opened; no message was sent.');
MAPI_E_BAD_RECIPTYPE:
Showmessage('The type of a recipient was not MAPI_TO, MAPI_CC, or MAPI_BCC. No message was sent.');
MAPI_E_FAILURE:
Showmessage('One or more unspecified errors occurred; no message was sent.');
MAPI_E_INSUFFICIENT_MEMORY:
Showmessage('There was insufficient memory to proceed. No message was sent.');
MAPI_E_LOGIN_FAILURE:
Showmessage('There was no default logon, and the user failed to log on successfully when the logon dialog box was displayed. No message was sent.');
MAPI_E_TEXT_TOO_LARGE:
Showmessage('The text in the message was too large to sent; the message was not sent.');
MAPI_E_TOO_MANY_FILES:
Showmessage('There were too many file attachments; no message was sent.');
MAPI_E_TOO_MANY_RECIPIENTS:
Showmessage('There were too many recipients; no message was sent.');
MAPI_E_UNKNOWN_RECIPIENT:
Showmessage('A recipient did not appear in the address list; no message was sent.');
MAPI_E_USER_ABORT:
Showmessage('The user canceled the process; no message was sent.');
else
Showmessage('MAPISendMail failed with an unknown error code.');
Result := False;
end;
end;
I can see two obvious mistakes. Firstly you use a lot of implicit temporary AnsiString variables whose lifetime is unclear. This happens whenever you do
PAnsiChar(AnsiString(...))
The compiler makes a temporary AnsiString local variable. You are relying on the compiler making enough of them, one for each distinct string.
When you do this inside a loop, the compiler will have one implicit local variable shared between all iterations. That's not enough. You might get away with this because the memory manager doesn't happen to re-use the memory. But your code is still wrong.
I would create a TList<AnsiString> to hold the numerous AnsiString variables. Every time you need to get a PAnsiChar do this:
astr := AnsiString(...);
TempAnsiStrings.Add(astr);
... := PAnsiChar(astr);
where TempAnsiStrings is your list that holds temporary AnsiString objects. Wrap this up in a nested function for ease of use.
function GetPAnsiChar(const str: string): PAnsiChar;
var
astr: AnsiString;
begin
astr := AnsiString(str);
TempAnsiStrings.Add(astr);
Result := PAnsiChar(astr);
end;
Obviously you need to instantiate the list, and destroy it, but I trust you already know how to do that.
Your other problem are the calls to StrDispose. You have to remove them since the compiler is managing lifetime. Call StrDispose to free memory allocated with StrNew or StrAlloc. You don't allocate this way so there is no place for StrDispose.
Rather than using explicit memory allocation for list of files, a dynamic array would be cleaner. This would have the other benefit of avoiding the possibility of leaking that your code currently has due to its missing finally block.
Related
I have the following code I use for retrieving mails from a POP3 account.
It is working very well most of the time, but from time to time there are some mails where it doesn't retrieve the body.
If I test the IdMessage.MessageParts.Count it says that it is 0 - if I use another mailclient to retrieve the mail there is no problem.
I have a TIdPOP3 and a TIdMessage component on the form.
The connection is OK as there may be some mails that are show OK.
I can't figure out any system in which mails are not shown correct and which are not. But there might be one.
I use Delphi XE3 and the Indy is version 10.5.9.0
procedure TfrmJsMailCollect.lstMailsClick(Sender: TObject);
var
MailBody: string;
intIndex: integer;
begin
if (lstMails.Items.Count = 0) or (lstMails.SelCount = 0) then
Exit;
MailBody := '';
try
begin
mmoBody.Clear;
lstMails.Selected.SubItems.Strings[0];
lstMails.Selected.ImageIndex := 4;
conPOP3.Retrieve(lstMails.Selected.Index + 1, IdMessage);
for intIndex := 0 To Pred(IdMessage.MessageParts.Count) do
begin
if (IdMessage.MessageParts.Items[intIndex] is TIdAttachmentFile) then
begin // Attachments are skipped
end
else
begin // body text
if Pos('text/plain', IdMessage.MessageParts.Items[intIndex].ContentType) <> 0 then
begin
if TIdText(IdMessage.MessageParts.Items[intIndex]).Body.Text <> '' then
begin
MailBody := MailBody + TIdText(IdMessage.MessageParts.Items[intIndex]).Body.Text;
mmoBody.Lines.Add(MailBody);
MemoValidate;
end;
end;
end;
end;
end;
mmoBody.CaretPos.SetLocation(0, 0);
Application.ProcessMessages;
except
Logfile.Error('F_JsMailCollect.lstMailsClick - ' + cxGetResourceString(#sLangPop3ErrorReading));
end;
end;
With advice from Remy Lebeau and searching the web I ended up with the code below. This does the trick for now, but I would like to improve it so that the memo on my form only shows a nice message that would be readable for everyone - but that may come later.
procedure TfrmJsMailCollect.lstMailsClick(Sender: TObject);
var
MailBody: string;
i: integer;
ContentType: string;
begin
if (lstMails.Items.Count = 0) or (lstMails.SelCount = 0) then
Exit;
try
MailBody := '';
mmoBody.Clear;
lstMails.Selected.SubItems.Strings[0];
lstMails.Selected.ImageIndex := 4;
conPOP3.Retrieve(lstMails.Selected.Index + 1, IdMessage);
ContentType := IdMessage.ContentType;
case PosInStrArray(ContentType, ['multipart/mixed', 'multipart/alternative', 'text/html', 'text/plain'], False) of
0: begin { multipart/mixed }
for i := 0 To Pred(IdMessage.MessageParts.Count) do
begin
if TIdText(IdMessage.MessageParts.Items[i]).Body.Text <> '' then
begin
MailBody := MailBody + TIdText(IdMessage.MessageParts.Items[i]).Body.Text;
mmoBody.Lines.Add(MailBody);
MemoValidate;
end;
end;
end;
1: begin { multipart/alternative }
for i := 0 To Pred(IdMessage.MessageParts.Count) do
begin
if TIdText(IdMessage.MessageParts.Items[i]).Body.Text <> '' then
begin
MailBody := MailBody + TIdText(IdMessage.MessageParts.Items[i]).Body.Text;
mmoBody.Lines.Add(MailBody);
MemoValidate;
end;
end;
end;
2: begin { text/html }
mmoBody.Lines := IdMessage.Body;
MemoValidate;
end;
3: begin { text/plain }
mmoBody.Lines := IdMessage.Body;
MemoValidate;
end;
else
// nothing supported to display...
end;
mmoBody.CaretPos.SetLocation(0, 0);
Application.ProcessMessages;
except
Logfile.Error('F_JsMailCollect.lstMailsClick - ' + cxGetResourceString(#sLangPop3ErrorReading));
end;
end;
Not all email content is parsed into the TIdMessage.MessageParts collection. MIME parts and attachments are, but other content gets parsed into the TIdMessage.Body instead, which you are completely ignoring. You need to look at the TIdMessage.ContentType when deciding where to extract content from. Attachments will always be in TIdMessage.MessageParts, but text may or may not be, depending on the TIdMessage.ContentType.
I have a function declare like this :
function execProc(ProcName,InValues:PChar;out OutValues:PChar):integer; //The "OutValues" is a out parameter.
And I call this function like this:
procedure TForm1.Button6Click(Sender: TObject);
var
v:integer;
s:pchar;
begin
Memo1.Clear;
v := execProc(pchar('PROC_TEST'),pchar('aaa'),s);
showmessage(inttostr(v)); //mark line
Memo1.Lines.Add(strpas(s));
end;
when i delete the mark line(showmessage(inttostr(v))),i will have a correct result display in the Memo1,but if i keep use the showmessage(), the memo1 will dispaly an error string : "Messag" ,Why?
Thanks for any help!
function execProc(ProcName,InValues:PChar;out OutValues:PChar):integer;
var
str: TStrings;
InValue,OutValue: string;
i,j,scount: integer;
begin
Result := -100;
i := 0;
j := 0;
str := TStringList.Create;
try
sCount := ExtractStrings(['|'], [], InValues, str);
with kbmMWClientStoredProc1 do
begin
Close;
Params.Clear;
StoredProcName := StrPas(ProcName);
FieldDefs.Updated := False;
FieldDefs.Update;
for i := 0 to Params.Count - 1 do
begin
if (Params[i].ParamType = ptUnknown) or
(Params[i].ParamType = ptInput) or
(Params[i].ParamType = ptInputOutput) then
begin
inc(j);
InValue := str[j-1];
Params[i].Value := InValue;
end;
end;
try
ExecProc;
for i := 0 to Params.Count - 1 do
begin
if (Params[i].ParamType = ptOutput) or
(Params[i].ParamType = ptInputOutput) then
OutValue := OutValue + '|' + Params[i].AsString;
end;
OutValues := PChar(Copy(OutValue,2,Length(OutValue)-1));
Result := 0;
except
on E:Exception do
begin
if E.Message = 'Connection lost.' then Result := -101;//服务器连接失败
if E.Message = 'Authorization failed.' then Result := -102;//身份验证失败
Writelog(E.Message);
end;
end;
end;
finally
str.Free;
end;
end;
The problem is in the design of your interface and the use of PChar.
OutValues := PChar(Copy(OutValue,2,Length(OutValue)-1));
This is implemented by making an implicit, hidden, local string variable which holds the value
Copy(OutValue,2,Length(OutValue)-1)
When the function returns, that string variable is destroyed and so OutValues points at deallocated memory. Sometimes your program appears to work but that's really just down to chance. Any small change can disturb that, as you have observed.
The problem is easy enough to fix. Simply use string parameters rather than PChar. This will make the code easier to read as well as making it work correctly.
function execProc(ProcName, InValues: string; out OutValues: string): integer;
I cannot delete files to Recycle Bin.
VAR SHFileOpStruct: TSHFileOpStruct;
begin
with SHFileOpStruct do
begin
wnd := Handle;
wFunc := FO_DELETE;
pFrom := PChar(FileName);
fFlags:= 0;
pTo := NIL;
hNameMappings:= NIL;
lpszProgressTitle:= NIL;
end;
Result:= SHFileOperation(SHFileOpStruct);
end;
I can delete files in this format: '1.xyz' but not in this format '12.xyz' (file name is longer than 1 character).
According to the documentation of SHFileOperation you should not use GetLastError to see if the operation succeeds. Check the Result of the function and use the documentation to figure out the error it returns. That should give you a better clue what the problem is.
EDIT:
Best guess from reading the documentation:
pFrom
Although this member is declared as a
single null-terminated string, it is
actually a buffer that can hold
multiple null-delimited file names.
Each file name is terminated by a
single NULL character. The last file
name is terminated with a double NULL
character ("\0\0") to indicate the end
of the buffer
So you should make sure pFrom is ended with a double 0. Try the following
pFrom := PChar(FileName + #0);
Also, what Delphi version are you using?
EDIT2:
Also make sure the structure is properly initialized to 0. Uncomment the FillChar
This works for me:
function DeleteToRecycleBin(WindowHandle: HWND; Filename: string; Confirm: Boolean): Boolean;
var
SH: TSHFILEOPSTRUCT;
begin
FillChar(SH, SizeOf(SH), 0);
with SH do
begin
Wnd := WindowHandle;
wFunc := FO_DELETE;
pFrom := PChar(Filename + #0);
fFlags := FOF_SILENT or FOF_ALLOWUNDO;
if not Confirm then
begin
fFlags := fFlags or FOF_NOCONFIRMATION
end;
end;
Result := SHFileOperation(SH) = 0;
end;
You may want to set the fFlags := FOF_SILENT + FOF_ALLOWUNDO + FOF_NOCONFIRMATION
Sometimes I experience random crashes in my Delphi program. My program halts, and the Debugger outputs:
Invalid address specified to RtlFreeHeap( 06450000, 08387460 )
What does that mean? And what can possibly cause it?
This is where the CPU Inspector stopped:
77BA0845 C6052582BD7700 mov byte ptr [$77bd8225],$00
Please note that they are very random (for me). Sometimes they don't appear at all.
I am using the Skype4COM.dll from Skype - there's no source though.
In case you need it, here is the code. I have commented most of the calls to Synchronize, so you know what they do.
////////////////////////////////////////////////////////////////////////////////
/// Execute
////////////////////////////////////////////////////////////////////////////////
procedure TContactDeletor.Execute;
Var
I : Integer;
UserObj : PUser;
User : IUser;
PauseEvent : TEvent;
begin
inherited;
FreeOnTerminate := True;
if Terminated then
Exit;
CoInitialize(Nil);
// The F-Flags are to make sure TSkype events do not fire (from my Main Thread)
FAllowUI := False;
FUserIsBeingDeleted := False;
FUseGroupUsersEvent := False;
FUseRenameEvent := False;
SkypeThr := TSkype.Create(Nil);
SkypeThr.Attach(10,False);
SkypeThr.Cache := False;
MyList := TStringList.Create;
PauseEvent := TEvent.Create(True);
try
// This fills my Stringlist
Synchronize(GrabList);
if Terminated then Exit;
iMax := MyList.Count;
// This sets the Max of my Progressbar
Synchronize(SetMax);
Try
for I := 0 to MyList.Count - 1 do
begin
{while SkypeThr.AttachmentStatus <> apiAttachSuccess do
begin
SkypeThr.Attach(10,False);
Synchronize(Procedure Begin Log('Skype Unavailable - Trying to reconnect ...'); End);
PauseEvent.WaitFor(5000);
end; }
CurUser := '';
User := SkypeThr.User[MyList[I]];
CurUser := MyList[I];
Try
User.IsAuthorized := False;
User.BuddyStatus := budDeletedFriend;
Except on E:Exception do
begin
ExErr := E.Message;
ExLog := 'Error while deleting contacts: ';
ExMsg := 'An Error has occured while deleting contacts: ';
Synchronize(
Procedure
Begin
Log(ExLog+ExErr+sLineBreak+' - Last logged Handle: '+CurUser);
End
);
end;
end;
iProgress := I+1;
// This updates my log and my progressbar.
Synchronize(UpdatePG);
PauseEvent.WaitFor(100);
if (I mod 200 = 0) and (I > 0) then
begin
// Calls to Synchronize updates my log
Synchronize(SyncPauseBegin);
PauseEvent.WaitFor(3000);
Synchronize(SyncPauseEnd);
end;
end;
// Except
Except on E:Exception do
begin
ExErr := E.Message;
ExLog := 'Error while deleting contacts: ';
ExMsg := 'An Error has occured while deleting contacts: ';
Synchronize(
Procedure
Begin
Log(ExMsg+ExErr+sLineBreak+' - Last logged Handle: '+CurUser);
ErrMsg(ExMsg+ExErr+sLineBreak+sLineBreak+' - Last logged Handle: '+CurUser);
End
);
Exit;
end;
end;
// This synchronizes my visual list.
Synchronize(SyncList);
finally
FUserIsBeingDeleted := False;
FUseGroupUsersEvent := True;
FUseRenameEvent := True;
FAllowUI := True;
Synchronize(
Procedure
Begin
frmMain.UpdateStatusBar;
PleaseWait(False);
ToggleUI(True);
end);
PauseEvent.Free;
SkypeThr.Free;
MyList.Free;
CoUninitialize;
end;
end;
Najem was correct, it is because your heap is corrupted. To debug it easier, you should enable PageHeap, also, use the debug CRT (or debug delphi runtime) as much as possiable until you find out what's corrupting your memory.
A lot of the time, the corruption may only spill over a few bytes. Then your application can run fine for a very long time, so you will not notice anything wrong until much later, if at all.
Try to exit your application cleanly when your looking for memory corruption bugs, dont just stop the debugger, when your process is exiting, it should "touch" most of the memory it allocated earlier and it will give you a chance to detect the failure.
I have created a program that can read email from Exchange 2007. However, it can only read the body of the email in plain-text format. When I tried to retrieve email in HTML format, my software cannot read the body and it always blank. I am using Delphi 2007 and IMAP 9.
Update:
Here is my code:
procedure TForm1.tmrCekTimer(Sender: TObject);
var
TheFlags: TIdMessageFlagsSet;
TheUID: string;
TheMsg: TIdMessage;
MailBoxName: string;
MyClass: TComponent;
begin
MailBoxName := 'INBOX';
if TheImap.SelectMailBox(MailBoxName) = False then
begin
Screen.Cursor := crDefault;
ShowMessage('Error selecting '+MailBoxName);
Exit;
end;
TheMsg := TIdMessage.Create(nil);
nCount := TheImap.MailBox.TotalMsgs;
TheMsg.ContentType := 'multipart/alternative';
TheMsg.Encoding := meMime;
if nCount = 0 then begin
StringGrid1.RowCount := 2;
StringGrid1.Cells[0, 1] := '';
StringGrid1.Cells[1, 1] := '';
StringGrid1.Cells[2, 1] := '';
StringGrid1.Cells[3, 1] := '';
ShowMessage('There are no messages in '+MailBoxName);
end else begin
StringGrid1.RowCount := nCount + 1;
for i := 0 to nCount-1 do begin
TheImap.GetUID(i+1, TheUID);
TheImap.UIDRetrieveFlags(TheUID, TheFlags);
TheImap.UIDRetrieve(TheUID, TheMsg);
//TheImap.UIDRetrieveHeader(TheUID, TheMsg);
StringGrid1.Cells[0, i+1] := IntToStr(i+1);
StringGrid1.Cells[1, i+1] := TheMsg.From.Address;
//StringGrid1.Cells[1, i+1] := TheUID;
if mfSeen in TheFlags then
StringGrid1.Cells[2, i+1] := 'Yes'
else begin
StringGrid1.Cells[2, i+1] := 'No';
end;
end;
end;
The contents of MIME-encoded emails, such as HTML emails (if plain-text and/or attachments are also present) are stored in the TIdMessage.MessageParts property, not in the TIdMessage.Body property. You need to look at the email's actual ContentType property to know which property TIdMessage parsed the email into.
Using MAPI, I usually try to get the PR_BODY_HTML property as string; if that’s empty, I retrieve the PR_HTML property.
const
PR_HTML = $10130102;
PR_BODY_HTML = $1013001E;
This usually works for me. Of course, maybe you’re using different technology altogether, but you’re not giving us much to work with...