How to get a parsable response from SendCmd in Indy? - delphi

If I use TIdImap4.SendCmd to manually send an unsupported command to Indy, I'm a little bit confused how I retrieve the full response to manually parse it.
I'm sending the following command to manually request the Gmail labels for a message since this is not yet supported by Indy:
IMAP.SendCmd(ImapCmdNum(),'UID FETCH '+uid+' (X-GM-LABELS)',['OK','BAD','NO'], false);
After calling this command, I checked my Indy Log file and it's successfully receiving an appropriate response from the server:
Sent 9/19/2015 11:10:40 AM: C5 UID FETCH 2385 (X-GM-LABELS)<EOL>
Recv 9/19/2015 11:10:40 AM: * 542 FETCH (X-GM-LABELS (testlabel) UID 2385)<EOL>C5 OK Success<EOL>
But now I can't seem to get any part of that response other than 'OK' from Indy. I've tried the following in the debugger and none of them have the raw response or anything else I could possibly manually parse:
IMAP.LastCmdResult = ('OK', $2521E60, nil, $2521EC0)
IMAP.LastCmdResult.Text = ()
IMAP.LastCmdResult.Code = 'OK'
IMAP.LastCmdResult.NumericCode = 0
IMAP.LastCmdResult.FormattedReply = ()
From the SendCmd documentation:
SendCmd is an overloaded function used to send the command specified
in AOut to the peer connection.
SendCmd uses IOHandler to write the command in AOut to the peer
connection.
AResponse indicates the response allowed for the command.
SendCmd calls GetResponse to determine if the response from the peer
connection is allowed. If the response is not allowed, an exception is
raised during processing in GetResponse.
When AResponse is contains -1, GetResponse is called with an empty
array to indicate that any response code is permitted for the command.
Otherwise, the value in AResponse is used to valid the response code.
Use LastCmdResult to access the numeric and text portions of the
response for the command.
My understanding of this is that I should be using LastCmdResult to access various "portions of the response", but none of them have the raw response or any part of the response except "OK", so how do I get something parsable from the response to SendCmd?

The text you are looking for is, in fact, in the LastCmdResult.Text property. The debugger is not showing it to you, but that is where the label data is.
As I told you 2 months ago in comments to my other answer that you linked to:
Look at the implementation of TIdIMAP4.UIDRetrieveFlags(). It calls SendCmd() followed by ParseLastCmdResult() to parse the returned flags. You will have to replicate the same logic, substituting fdGmailLabels where fdFlags is currently being used (minus the call to ParseMessageFlagString() that is parsing the flags string to a TIdMessageFlagsSet).
If you look at the implementation of TIdIMAP4.UIDRetrieveFlags() and then look at your code, you are not even calling SendCmd() correctly to begin with. You are passing the wrong value to the ATag parameter (unless ImapCmdNum() is simply calling TIdIMAP4.NewCmdCounter - TIdIMAP4 needs to generate the command counters so it can match them to the replies), and moe importantly you are passing the wrong values to the AExpectedResponses parameter.
Try this instead (I tested it and it works):
type
TIdIMAP4Access = class(TIdIMAP4);
TIdIMAPLineStructAccess = class(TIdIMAPLineStruct);
var
uid: string;
labels: string;
begin
...
uid := ...;
labels := '';
IMAP.SendCmd('UID FETCH ' + uid + ' (X-GM-LABELS)', ['FETCH','UID']);
if IMAP.LastCmdResult.Code = IMAP_OK then
begin
if IMAP.LastCmdResult.Text.Count > 0 then
begin
// The requested data is in IMAP.LastCmdResult.Text[0].
// You can either parse it manually, or use the below
// code to let TIdIMAP4 parse it for you...
if TIdIMAP4Access(IMAP).ParseLastCmdResult(IMAP.LastCmdResult.Text[0], 'FETCH', ['X-GM-LABELS']) then begin
labels := TIdIMAPLineStructAccess(TIdIMAP4Access(IMAP).FLineStruct).IMAPValue;
end;
end;
end;
...
end;

Related

Inserting name into database, getting korean signs as output

Trying to insert simple xml file with one row in IIB with simple message flow into Oracle XE DB. Message flow works fine and inserts data into database, but data written in db is different from starting data. For example, as I'm trying to insert my name "Dino" I'd get Korean/Japanese/Chinese signs in return.
I've tried changing XML formats thinking there might be problem, but I suppose it has to do with encoding.
Input:
Output in DB:
This is how my compute node looks like:
CREATE COMPUTE MODULE SimpleDB_mf_Compute
CREATE FUNCTION Main() RETURNS BOOLEAN
BEGIN
CALL CopyMessageHeaders();
-- CALL CopyEntireMessage();
INSERT INTO Database.dkralj.emp VALUES(InputRoot.XMLNSC.emp.name);
SET OutputRoot.XMLNSC.DBINSERT.STATUS='SUCCESS';
RETURN TRUE;
END;
CREATE PROCEDURE CopyMessageHeaders() BEGIN
DECLARE I INTEGER 1;
DECLARE J INTEGER;
SET J = CARDINALITY(InputRoot.*[]);
WHILE I < J DO
SET OutputRoot.*[I] = InputRoot.*[I];
SET I = I + 1;
END WHILE;
END;
CREATE PROCEDURE CopyEntireMessage() BEGIN
SET OutputRoot = InputRoot;
END;
END MODULE;
Looking at the IBM documentation for the INSERT statement in ESQL it might be worth trying.
INSERT INTO Database.dkralj(NAME) VALUES(InputRoot.XMLNSC.emp.name);
If weird things are still happening then I'd try a string constant to avoid any issues with character coding in the input message.
INSERT INTO Database.dkralj(NAME) VALUES('TheEmpValue');
Before this statement in your code
SET OutputRoot.XMLNSC.DBINSERT.STATUS='SUCCESS';
You should check for success or otherwise by using the inbuilt SQLSTATE, SQLCODE, SQLERRORTEXT to check the result of your call.
IF NOT ((SQLCODE = 0) OR (SQLSTATE = '01000' AND SQLNATIVEERROR = 8153)) THEN
-- Do something about the error.
-- The check of SQLSTATE and SQLNATIVEERROR covers warnings
-- The 8153 is for Microsoft SQL Server other databases may use a different value
END IF;
Also check the codepages aka CodedCharSetId of the source system data, the message in IIB and the default codepage of the database.
Use mqsicvp MYBROKER -n ODBC_DB_NAME to get other details about the connection you need to use -n to get the details.
Use something like DBeaver to add some data. Have a look at the datatype specified for the field.
As per your comment below and my response here is an example of a PASSTHRU statement. Note the use of the ? to avoid SQL Injection.
PASSTHRU('SELECT RTRIM(A.EMPLID) AS EMPLID,
RTRIM(A.ADDRESS_TYPE) AS ADDRESS_TYPE,
RTRIM(A.ADDR_TYPE_DESCR) AS ADDR_TYPE_DESCR,
CAST(RTRIM(A.EFFDT) AS DATE) AS EFFDT,
RTRIM(A.EFF_STATUS) AS EFF_STATUS,
RTRIM(A.ADDRESS1) AS ADDRESS1,
RTRIM(A.ADDRESS2) AS ADDRESS2,
RTRIM(A.ADDRESS3) AS ADDRESS3,
RTRIM(A.ADDRESS4) AS ADDRESS4,
RTRIM(A.CITY) AS CITY,
RTRIM(A.STATE) AS STATE,
RTRIM(A.POSTAL) AS POSTAL
FROM ADDRESS_VW AS A
WHERE UPPER(A.EMPLID) = ?') VALUES(AggrRef.EmployeeID)

Delphi - Soap array item value

I am writing a SOAP webservice client (which is provided by others). I have imported WSDL and called request, servise respondes an XML like below. I can reach (DeliveryNumberList array's item DeliveryNumber) DeliveryNumber's PackingSlipNo and VendorAccount attributes but i need to reach DeliveryNumber's value (008740774). In the class file (which Delphi generated from WSDL) there is no option to reach this value. Someone have any idea?
<ns1:Results>
<ns1:Result>
<ns1:Status>true</ns1:Status>
<ns1:Message>Başarılı</ns1:Message>
<ns1:VendorAccount/>
<ns1:DeliveryNumberList>
<ns1:DeliveryNumber PackingSlipNo="X100327233" VendorAccount="0002230728">008740774</ns1:DeliveryNumber>
</ns1:DeliveryNumberList>
</ns1:Result>
</ns1:Results>
If you are able to extract the value of VendorAccount, it should be possible to extract also the value of DeliveryNumber.
May be you could show some code how you extract the value of VendorAccount.
PS: can not add comment because not enough reputation, therefore posting as answer
Here is some code that extracts the value from a XML:
myXML.LoadFromFile(FileOpen.FileName);
memo1.Lines.Add('LocalName:'+myXML.DocumentElement.LocalName);
nrChildNodes := myXML.DocumentElement.ChildNodes.Count;
memo1.Lines.Add('ChildNodes:'+inttostr(nrChildNodes) );
aNode := myXML.DocumentElement.ChildNodes[0].ChildNodes.FindNode('DeliveryNumberList');
if aNode <> nil then
begin
memo1.Lines.Add('aNode.LocalName:'+aNode.LocalName);
memo1.Lines.Add('DeliveryNumber='+aNode.ChildValues['DeliveryNumber']);
end
else memo1.Lines.Add('aNode = nil');

How to save an email attachment to file

I would to save an email attachment to a file using a TIdImap4 object of Indy Ver.10.
I get the UID of the email, then I use this code:
lMsg := TIdMessage.Create(Self);
lImap.UIDRetrieveStructure(lUid, lMsg);
lMsg.MessageParts.CountParts;
if lMsg.MessageParts.AttachmentCount > 0 then
for lJ := 0 to lMsg.MessageParts.Count - 1 do
if (lMsg.MessageParts[lJ] is TIdAttachment) and
SameText(lMsg.MessageParts[lJ].Name, 'MyAttachment') then
lImap.UidRetrievePartToFile(lUid, lJ, lDimAllegato, lFileName, Trim(lMsg.MessageParts[lJ].ContentTransfer))
This worked until lMsg.MessageParts[lJ].ContentType = 'Text/Plain' and
lMsg.MessageParts[lJ].ContentTransfer = '7bit', now UidRetrievePartToFile() returns False and no file is created. I suppose because
lMsg.MessageParts[lJ].ContentType = 'application/octet-stream' and
lMsg.MessageParts[lJ].ContentTransfer = 'base64'.
I'm not skilled on this topic, what I need to change in code in order to save this type of attachment?
I also tried with: TIdAttachment(lMsg.MessageParts[lJ]).SaveToFile(lFileName)
and similar, but the file created was always empty.
Using UIDRetrieveStructure() with a TIdMessage is going to fill the TIdMessage.MessageParts with a lot of TIdttachment objects, never any TIdText objects, and not all of the objects are going to represent actual attachments. You are using the TIdAttachment indexes as the APartNum parameter of UIDRetrievePartToFile(), which might not be accurate.
And you can't use TIdAttachment.SaveToFile() when using UIDRetreiveStructure(), because no actual data has been downloaded, only the structure of the email, which then allows you to download the data for the specific elements you want.
I suggest you use the other overloaded version of UIDRetrieveStructure() that fills a TIdImapMessageParts instead. Amongst other things, TIdImapMessagePart gives you an exact ImapPartNumber that you can then give to UIDRetrievePartToFile() (as well as the ContentTransferEncoding):
lParts := TIdImapMessageParts.Create(nil);
try
lImap.UIDRetrieveStructure(lUid, lParts);
for lJ := 0 to lParts.Count - 1 do
begin
if (lParts[lJ] is the desired attachment) then
begin
lImap.UidRetrievePartToFile(lUid, lParts[lJ].ImapPartNumber, lDimAllegato, lFileName, lParts[lJ].ContentTransferEncoding);
end;
end;
finally
lParts.Free;
end;

Serving files with IdHTTPServer when the files are being written

I'm working with a TIdHTTPServer to serve files to clients, using the ResponseInfo->ServeFile function. This works fine for files that are "static": not being written by some other process. As far as I can see from the code, the ServeFile function internally uses a TIdReadFileExclusiveStream, which disallows me from reading a file being written, but I need to be able to send also files that are being written by some other process.
So, I moved to create a FileStream myself and use the ContentStream property to return it to the client, but I get a 0 bytes file in the client (for any file, being written or not), and I can't see what I'm missing or doing wrong. Here is the code I'm using on the OnCommandGet event handler:
AResponseInfo->ContentStream = new TFileStream(path, fmOpenRead | fmShareDenyNone);
AResponseInfo->ContentStream->Position = 0;
AResponseInfo->ContentLength = AResponseInfo->ContentStream->Size;
AResponseInfo->ResponseNo = 200;
AResponseInfo->WriteHeader();
AResponseInfo->WriteContent();
The ContentLength property at this point has a valid value (i.e., the file size when calling ContentStream->Size), and that's what I would like to send to the client, even if the file changes in between.
I have tried removing the WriteContent() function, the WriteHeader(), but the results are the same. I searched for some examples but the few I found are more or less the same than this code, so I don't know what's wrong. Most examples don't include the WriteContent() call, that's why I have tried removing them, but there doesn't seem to be any difference.
As a side note: the files being written take 24 hours to finish writing, but that's to be expected from the client side: I just need the bytes already written at the time of the request (even somewhat less is valid). The files will never get deleted: they will just keep getting bigger.
Any ideas?
Update
Using Fiddler, I get some warnings on protocol violations, that would be related to this. I get, for instance:
Content-Length mismatch: Response Header indicated 111,628,288 bytes, but server sent 41 bytes
The content length is correct, it's the file size, but I don't know what I'm doing wrong that makes the app sent just 41 bytes.
WriteHeader() and WriteContent() expect the ContentStream to be complete and unchanging at the time they are called. WriteHeader() creates a Content-Length header using the current ContentStream->Size value if the AResponseInfo->ContentLength property is -1 (you are actually setting the value yourself), and WriteContent() sends only as many bytes as the current ContentStream->Size value says. So your client is receiving 0 bytes because the file Size is still 0 at the time you are calling WriteHeader() and WriteContent().
Neither ServeFile() nor ContentStream are suitable for your needs. Since the file is being written live, you do not know the final file size when the HTTP headers are created and sent to the client. So you must use HTTP 1.1's chunked transfer coding to send the file data. That will allow you to send the file data in chunks as the file is being written, and then signal the client when the file is finished.
However, TIdHTTPServer does not natively support sending chunked responses, so you will have to implement it manually, eg:
TFileStream *fs = new TFileStream(path, fmOpenRead | fmShareDenyNone);
try
{
AResponseInfo->ResponseNo = 200;
AResponseInfo->TransferEncoding = "chunked";
AResponseInfo->WriteHeader();
TIdBytes buffer;
buffer.Length = 1024;
do
{
int NumRead = fs->Read(&buffer[0], 1024);
if (NumRead == -1) RaiseLastOSError();
if (NumRead == 0)
{
// check for EOF, unless you have another way to detect it...
Sleep(1000);
NumRead = fs->Read(&buffer[0], 1024);
if (NumRead <= 0) break;
}
// send the current chunk
AContext->Connection->IOHandler->WriteLn(IntToHex(NumRead));
AContext->Connection->IOHandler->Write(buffer, NumRead);
AContext->Connection->IOHandler->WriteLn();
}
while (true);
// send the last chunk to signal EOF
AContext->Connection->IOHandler->WriteLn("0");
// send any trailer headers you need, if any...
// finish the transfer encoding
AContext->Connection->IOHandler->WriteLn();
}
__finally
{
delete fs;
}
The final working code is:
std::unique_ptr< TFileStream >fs(new TFileStream(path, fmOpenRead | fmShareDenyNone));
fs->Position = 0;
__int64 size = fs->Size;
AResponseInfo->ContentLength = size;
AResponseInfo->ResponseNo = 200;
AResponseInfo->WriteHeader();
AContext->Connection->IOHandler->Write(fs.get(), size);
This allows the client to receive up to size bytes of the original file, even if the file is being written to at the same time.
For some reason passing the ContentStream did not return any content to the client, but doing the IOHandler->Write directly (which is what the ServeFile ends doing internally) works fine.

EIPHTTPProtocolExceptionPeer exception using PutBlock with array of bytes all set to zero

Using Delphi XE2 Update 3 I'm having a problem uploading a block of zero bytes to Azure. When I say zero bytes, I mean an array of bytes with each element set to zero (it's not zero length).
For example:
var
ConInfo: TAzureConnectionInfo;
RespInfo: TCloudResponseInfo;
BlobService: TAzureBlobService;
Content: TBytes;
MD5: String;
Hasher: TIdHashMessageDigest5;
begin
ConInfo:=TAzureConnectionInfo.Create(self);
ConInfo.AccountName:='YOUR ACCOUNT NAME';
ConInfo.AccountKey:='YOUR ACCOUNT KEY';
ConInfo.Protocol:='http';
BlobService:=TAzureBlobService.Create(ConInfo);
RespInfo:=TCloudResponseInfo.Create;
SetLength(Content, 1048576); // 1 MByte
FillMemory(#Content[0], 1048576, 0); // << --- fill it with anything except 0 and it works
Hasher:=TIdHashMessageDigest5.Create;
MD5:=Data.Cloud.CloudAPI.EncodeBytes64(Hasher.HashBytes(Content));
Hasher.Free;
BlobService.PutBlock('CONTAINER NAME', 'BLOB NAME', MD5, Content, MD5, '', RespInfo);
If the above code is run then an exception is raised on the call to PutBlock:
Project Project1.exe raised exception class
EIPHTTPProtocolExceptionPeer with message 'HTTP/1.1 403 Server failed
to authenticate the request. Make sure the value of Authorization
header is formed correctly including the signature.'.
But if you change the FillMemory line so that it fills it with 1's (or anything but zero), then it works without error. Why?
Thanks for any help.
Unbelievably, it seems that the problem is the block ID value of ttgbNgpWctgMJ0MPORU+LA==
If you use it as a block ID then you get that error.

Resources