4

What is the correct code that I need to use to send one or more files of any type along with other parameters using Indy's idHTTP.post? (using Delphi 2009 and Indy 10)

The post in question calls a function in a commercial company's API (ElasticEmail) that sends out emails to the recipients held in one of the parameters. (A link to the documentation on the function I am calling is here. I have example code in C# and other languages from the company here and I have tried to replicate that code in my Delphi code below.

If, in Procedure btnSendbyElastic, I comment out the line Filenames.add(Afilename); so that the function Upload makes no attempt to attach a file,then the correct call seems to be made as the email gets sent successfully by the API. However, if I leave that line in so that the lines in function UpLoad

MimeStr := GetMIMETypeFromFile(filenames[i]);
FormData.Addfile('file'+inttostr(i), filenames[i],MIMEStr); 

do get executed, then no email is sent and the response from the server is

{"success":false,"error":"One of files has invalid characters in file name."}

(The file Afilename does exist at that location and I have tried with single and double backslashes)

Reading other SO posts on this topic I also tried replacing the file processing loop in Function UpLoad with the following loop instead

for i := 0 to filenames.Count - 1 do
    begin
    MimeStr := GetMIMETypeFromFile(filenames[i]); 
    FormData.AddFile('file'+inttostr(i), filenames[i],MIMEStr);
    AttachmentContent  := TFileStream.Create(filenames[i],fmOpenRead);
    try
        FormData.AddFormField(AttachmentContent.ToString,filenames[i]);
    finally
        AttachmentContent.free;
    end;
end; 

This time, even with a filename specified in Filenames.add(Afilename);, the email is sent correctly but the recipient sees no attachment.

Among many others, I have read these possible duplicate SO questions

Http Post with indy

Post a file through https using indy / delphi components

posting a file as part of a form

Nodejs POST request multipart/form-data

and in particular

Using the Indy TidHttp component to send email file attachments through sendgrid

(which is almost exactly what I am trying to do) but I still cannot see what I am doing wrong in my code and what I need to do to correct it.

Here is the code I am using (UPPER_CASE identifiers are constants defined elsewhere)

PS I'm in the UK so apologies for the time delay in responding to US comments/answers

function TForm1.Upload(url: string; params, filenames: Tstringlist): string;
var
 FormData : TIdMultiPartFormDataStream;
 MIMEStr, ResponseText : string;
 i : integer;
begin
  try
  FormData := TIdMultiPartFormDataStream.Create;
  for i := 0 to params.Count - 1 do
        FormData.AddFormField(params.Names[i],params.values[params.Names[i]]);
   for i := 0 to filenames.Count - 1 do
     begin
     MimeStr := GetMIMETypeFromFile(filenames[i]); 
     FormData.Addfile(filenames[i], filenames[i],MIMEStr);
     end;
  ResponseText :=IdHTTP1.Post(url, FormData);
  Memo1.Text := ResponseText; //debug
  finally
  FormData.free;
  end;
end;

procedure TForm1.btnSendbyElastic(Sender: TObject);
var
Params, Filenames : Tstringlist;
url, Afilename : string;
begin
Afilename := 'C:\\Users\\Admin\\Documents\\arrival and departure small.pdf';
Params := Tstringlist.Create;
Filenames  := Tstringlist.Create;
try
  Params.add('apikey=' + ELASTIC_MAIL_API_KEY) ;
  Params.add('from=' + ELASTIC_EMAIL_FROM_EMAIL) ;
  Params.add('fromname=' + ELASTIC_EMAIL_FROM_NAME) ;
  Params.add('Subject=' + 'The Subject') ;
  Params.add('bodyHtml=' + '<h1>Html Body</h1>') ;
  Params.add('bodyText=' + 'Text Body') ;
  Params.add('to=' + THE_RECIPIENT_ADDRESS) ;
  Filenames.add(Afilename); //*** comment out this line and an email is sent correctly
  url := ELASTIC_EMAIL_EMAIL_SEND  ;
  Upload (url , params, filenames );
finally
  Params.free;
  Filenames.free;
end;

The function GetMIMETypeFromFile is defined in the Indy unit idGlobalProtocols. I didn't write it, I just call it. But I have reproduced it here as requested

function GetMIMETypeFromFile(const AFile: TIdFileName): string;
var
  MIMEMap: TIdMIMETable;
begin
  MIMEMap := TIdMimeTable.Create(True);
  try
    Result := MIMEMap.GetFileMIMEType(AFile);
  finally
    MIMEMap.Free;
  end;
end;
user2834566
  • 775
  • 9
  • 22
  • I don't see why you need to add double slashes in the filename (c#, c require this, not Delphi). The error you get from Elastic is quite verbose, "invalid characters in filename". Try something simple like "c:\temp\test.txt". Please also include the code for `GetMIMETypeFromFile`, so we can try to reproduce the problem... – whosrdaddy Apr 20 '19 at 14:11
  • @whosrdaddy `GetMIMETypeFromFile()` is an Indy function. – Tom Brunberg Apr 20 '19 at 18:03
  • Ah, Tom Brunberg beat me to it. Anyway I have edited my post to include it as requested. I agree that I don't see why I need the double backslashes. I tried using them as the C# example had them and I've had to use them before when dealing with networking and, I think, some http calls. Anyway, they don't give a syntax error and I get the same results whether I use them or not. – user2834566 Apr 20 '19 at 21:46
  • I did try using a simpler pdf filename with no spaces in the filename or path and it made no difference - email sent but with no attachment. But I don't think the issue is with the filename itself. After all I have no idea what file my users are going to want to attach and cannot insist they only attach simple filenames. – user2834566 Apr 20 '19 at 21:48
  • I also tried commenting out the line `Params.add('bodyHtml=' ... `so that no html body was sent as I heard somewhere that this can cause issues with spam filters. But no difference whether there was a html body as well as textual one or not. – user2834566 Apr 20 '19 at 21:53

1 Answers1

3

I see a few problems with your code.

You are erroneously escaping \ characters in your file paths. That is necessary in languages like C and C++, but is not needed in Delphi at all, so get rid of it.

Change this:

Afilename := 'C:\\Users\\Admin\\Documents\\arrival and departure small.pdf';

To this:

Afilename := 'C:\Users\Admin\Documents\arrival and departure small.pdf';

The next problem I see is you are not naming the file attachment fields correctly when adding them to the TIdMultipartFormDataStream.

When calling AddFile(), you are passing the complete file path as-is to the AFieldName parameter, instead of using names like file0, file1, etc like shown in Elastic's examples.

Change this:

FormData.Addfile(filenames[i], filenames[i],MIMEStr);

To this 1:

FormData.AddFile('file'+IntToStr(i), filenames[i], MIMEStr);

1: FYI, there is no need to call GetMIMETypeForFile() manually, AddFile() calls GetMIMETypeForFile() internally for you if you do not provide a string for the AContentType parameter, eg FormData.AddFile('file'+IntToStr(i), filenames[i]);

You made a similar mistake when you tried to use AddFormField() instead of AddFile() to add attachments. You used each file's actual data content for the AFieldName parameter, instead of using the content for the AFieldValue parameter.

In that case, change this:

FormData.AddFormField(AttachmentContent.ToString,filenames[i]);

To this:

FormData.AddFormField('file'+IntToStr(i), AttachmentContent.ToString, '', MIMEStr, filenames[i]);

Or, since you were opening TFileStream objects yourself, you could use the overloaded AddFormField() method that takes a TStream as input (just be sure NOT to free the TStream objects until after you are done using the TIdMultipartFormDataStream!):

AttachmentContent := TFileStream.Create(filenames[i], fmOpenRead);
FormData.AddFormField('file'+IntToStr(i), MIMEStr, '', AttachmentContent, filenames[i]);

With that said, try something more like this:

function TForm1.Upload(url: string; params, filenames: TStrings): string;
var
 FormData : TIdMultiPartFormDataStream;
 ResponseText : string;
 i : integer;
begin
  FormData := TIdMultiPartFormDataStream.Create;
  try
    for i := 0 to params.Count - 1 do
      FormData.AddFormField(params.Names[i], params.ValueFromIndex[i]);

    for i := 0 to filenames.Count - 1 do
      FormData.AddFile('file'+IntToStr(i), filenames[i]);

    ResponseText := IdHTTP1.Post(url, FormData);
    Memo1.Text := ResponseText; //debug
  finally
    FormData.Free;
  end;
end;

procedure TForm1.btnSendbyElastic(Sender: TObject);
var
  Params, Filenames : TStringList;
  url, Afilename : string;
begin
  Afilename := 'C:\Users\Admin\Documents\arrival and departure small.pdf';
  Params := TStringList.Create;
  try
    Params.Add('apikey=' + ELASTIC_MAIL_API_KEY);
    Params.Add('from=' + ELASTIC_EMAIL_FROM_EMAIL);
    Params.Add('fromname=' + ELASTIC_EMAIL_FROM_NAME);
    Params.Add('Subject=' + 'The Subject');
    Params.Add('bodyHtml=' + '<h1>Html Body</h1>');
    Params.Add('bodyText=' + 'Text Body');
    Params.Add('to=' + THE_RECIPIENT_ADDRESS);

    Filenames := TStringList.Create;
    try
      Filenames.Add(Afilename);

      url := ELASTIC_EMAIL_EMAIL_SEND;
      Upload(url, params, filenames);
    finally
      Filenames.Free;
    end;
  finally
    Params.Free;
  end;
end;

Lastly, Elastic's documentation does not say anything about the encoding needed for filenames that contain non-ASCII/reserved characters in it. And there are conflicting standards as to how such filenames should be encoded when transmitted over HTTP. By default, TIdMultipartFormDataStream encodes filenames according to RFC 2047. If that turns out to be a problem for Elastic to handle (your example filename has space characters in it, I forget whether TIdMultipartFormDataStream RFC-encodes a filename due to spaces or not, hopefully not), you can disable TIdMultipartFormDataStream's default encoding by setting an affected file's TIdFormDataField.HeaderEncoding property to '8' (for 8-bit) and then you can set the TIdFormDataField.FileName property to whatever encoding you want:

with FormData.AddFile('file'+IntToStr(i), filenames[i]) do
begin
  HeaderEncoding := '8';
  FileName := EncodeFilenameMyWay(ExtractFileName(filenames[i]));
end;
Community
  • 1
  • 1
Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • I've given up with elasticmail and am trying mailgun's api. Using your code I got a 'not enough actual parameters' error on the line `FormData.AddFile('file'+IntToStr(i), filenames[i]);`. in Upload as it wanted 3 parameters, so I altered that to `MimeStr := GetMIMETypeFromFile(AttachFilePath + AttachFilename); FormData.Addfile('files['+AttachFilename+']', AttachFilePath + AttachFilename,MIMEStr);` Now I get error HTTP/1.1 400 bad request. The url and parameters are correct and I added the correct authorisation header using ` IdHTTP1.Request.CustomHeaders.Add(...)` – user2834566 May 08 '19 at 10:22
  • BTW I'm using Indy ver 10.2.5. User whosrdaddy said I should upgrade Indy but I'm nervous of doing that as it looks quite a complicated process and I don't want to break any of my other projects that are working happily. Shouldn't my version do a file upload via POST as well? – user2834566 May 08 '19 at 10:27
  • @user2834566 you are using an EXTREMELY old version of Indy 10 and definitely need to upgrade. The current version is 10.6.2.5498. Yes, your version can upload files via POST, but it likely has bugs,or less standards compliance, that have since been fixed over the years. A lot has changed between 10.2 and 10.6. – Remy Lebeau May 08 '19 at 15:59
  • You are right Remy. Yesterday I took the plunge and upgraded to what appeared to be the latest version using the overnight zip file. That gave me version 10.5498. The .bat file for compiling gave a 'device not ready' error but I was able to compile all the code packages separately and install the design packages in Delphi with no problem. Now your code works flawlessly (of course!). I should have taken note of all the comments you made in other posts about upgrading! – user2834566 May 09 '19 at 15:34
  • Upvote for the TIdMultiPartFormDataStream handling. After messing about with boundary's etc., this is so clean and friendly. – Jon Aug 25 '20 at 02:27