BookmarkSubscribeRSS Feed
freshstarter
Quartz | Level 8

Hello,

 

I am trying to send an email programmatically from SAS Studio using the Microsoft Graph API. I am able to send the email successfully along with an attachment.

 

However, the main issue is that the attachment is not in a proper format. For PDF, Excel, and ZIP files, I receive an “invalid format” error. Below is the code I used to send a ZIP file via email. Although I do receive the ZIP file, when I try to open it, I get the error: “Windows cannot open the folder. The compressed (zipped) file is invalid.”

 

Could you please advise whether there is an issue in the code or if Microsoft Graph cannot be used to send attachments in the correct format?

filename resp temp;
proc http 
url="https://login.microsoftonline.com/<tenant_id>/oauth2/v2.0/token"
in='grant_type=client_credentials&client_id=<client_id>&client_secret=<client_secret>&scope=https://graph.microsoft.com/.default' 
ct="application/x-www-form-urlencoded"
out=resp
method='POST';
run;
 
libname auth json fileref=resp;
data _null_;
set auth.root;
call symputx('access_token',access_token);
run;


/*--------------------------------*/
/* Encode attachment              */
/*--------------------------------*/

filename src "/mnt/discovery/ukida/_shared/test.zip" recfm=n;
filename tgt "/tmp/test.zip" recfm=n;

data _null_;
    infile src recfm=n lrecl=32767;
    file tgt recfm=n lrecl=32767;

    length buffer $32767;

    input buffer $char32767.;
    put buffer $char32767. @;   
run;

filename raw "/tmp/test.zip" recfm=n;
filename enc temp;

data _null_;
    infile raw recfm=n end=eof;
    file enc lrecl=76;

    length buffer $57;
    retain buffer '';

    input byte $char1.;
    buffer = cats(buffer, byte);

    if length(buffer) = 57 then do;
        put buffer $base64X76.;
        buffer = '';
    end;

    if eof and length(buffer) > 0 then do;
        put buffer $base64X76.;
    end;
run;
/*--------------------------------*/
/* Load into macro variable       */
/*--------------------------------*/
data _null_;
    infile enc truncover;
    length file_b64 $32767;
    retain file_b64 '';

    input line $char32767.;
    line = compress(line,,'kw');   

    file_b64 = cats(file_b64, line);

    call symputx('file_b64', file_b64, 'G');
run;


%put >>> BASE64 LENGTH = %length(&file_b64);

filename mail_in temp;

data _null_;
    file mail_in lrecl=32767;

    put '{';
    put '  "message": {';
    put '    "subject": "Test SAS Viya email with attachment",';

    put '    "body": {';
    put '      "contentType": "Text",';
    put '      "content": "Hello! This email has an attachment."';
    put '    },';

    put '    "toRecipients": [';
    put '      { "emailAddress": { "address": "test@org.com" } }';
    put '    ],';

    put '    "attachments": [';
    put '      {';
    put '        "@odata.type": "#microsoft.graph.fileAttachment",';
    put '        "name": "test.zip",';
    put '        "contentType": "application/zip",';
    put "        ""contentBytes"": ""&file_b64""";
    put '      }';
    put '    ]';

    put '  },';
    put '  "saveToSentItems": true';
    put '}';
run;

filename sendresp temp;

proc http
  url="https://graph.microsoft.com/v1.0/users/sasviya_dev@org.com/sendMail"
  method="POST"
  in=mail_in
  out=sendresp;
  headers
    "Authorization"="Bearer &access_token"
    "Content-Type"="application/json";
run;


data _null_;
  infile sendresp;
  input;
  put _infile_;
run;

 

3 REPLIES 3
gwootton
SAS Super FREQ

Sounds like the problem is with your code to generate the base64 content, so you are ending up with a bad attachment file.

If you replace your code from "filename src..." to "filename sendresp temp;" with this, does it work? Here I'm building the JSON structure around the base64 encoded file using put and then processing the attachment in 15,000 byte chunks into the contentBytes field, then at the end closing the JSON structure. 

/* Define the source file and the temporary file for the email request payload. */
filename src "/mnt/discovery/ukida/_shared/test.zip" recfm=f lrecl=15000;
filename mailin temp;

data _null_;
    file mailin recfm=n; /* Write the file as a byte stream without any record formatting */
    infile src length=bytes_read truncover end=eof; 
    
    /* Initialize variables */
    length 
        chunk $ 15000
        fmt_string $ 20
        b64_chunk $ 20000
    ;
    
    /* Build the JSON structure for the payload */
    if _n_ = 1 then do;
        put '{' /
            '  "message": {' /
            '    "subject": "Test SAS Viya email with attachment",' /
            '    "body": {' /
            '      "contentType": "Text",' /
            '      "content": "Hello! This email has an attachment." ' /
            '    },' /
            '    "toRecipients": [' /
            '      { "emailAddress": { "address": "test@org.com" } }' /
            '    ],' /
            '    "attachments": [' /
            '      {' /
            '        "@odata.type": "#microsoft.graph.fileAttachment",' /
            '        "name": "test.zip",' /
            '        "contentType": "application/zip",' /
            '        "contentBytes": "'; /* Leave this open */
    end;

    /* Process the attachment in 15000 byte chunks, encoding each chunk to base64 */
    input chunk $char15000.;
    b64_len = ceil(bytes_read / 3) * 4;
    fmt_string = compress("$base64x" || put(b64_len, best.));
    b64_chunk = strip(putc(substr(chunk, 1, bytes_read), fmt_string));
    actual_b64_len = length(b64_chunk);
    /* Write the base64 block directly into the open contentBytes quotes */
    put b64_chunk $varying20000. actual_b64_len @@; 

    /* Close the contentBytes quotes and JSON structure when the end of the file is reached. */
    if eof then do;
        put '"' / 
            '      }' /
            '    ]' /
            '  },' /
            '  "saveToSentItems": "true"' /
            '}';
    end;
run;

 

--
Greg Wootton | Principal Systems Technical Support Engineer
freshstarter
Quartz | Level 8

Thank you so much @gwootton . It worked. 

 

I am currently trying to create a single macro that can handle attachments of all file formats in an email. The idea is to be able to call the macro with the required parameters using the code below.

 

I have tried my best to get this working, but I have not been able to complete it successfully. It consistently throws the following error:

 

{"error":{"code":"BadRequest","message":"Unable to read JSON request payload. Please ensure Content-Type header is set and payload is of valid JSON format.","innerError":{"date":"2026-06-05T11:15:43","request-id":"2d98ce2d-3d92-44ad-ae29-dfb7e6917776","client-request-id":"2d98ce2d-3d92-44ad-ae29-dfb7e6917776"}}}
 
It would be very helpful if you could assist me in getting my code below to work. Thank you.

 

 

 

%macro send_mail(
    to=,
    cc=,
    subject=,
    body=,
    attachment=
);

    %local access_token has_cc has_attach content_type attachment_name;
    %let has_cc = %sysfunc(ifc(%length(&cc) > 0, 1, 0));
    %let has_attach = %sysfunc(ifc(%length(&attachment) > 0, 1, 0));

    %let SENDER_EMAIL = sasviya_dev@org.com;

    /*--------------------------------*/
    /* Read secrets                   */
    /*--------------------------------*/
    %macro read_secret(file,macvar);
    data _null_;
        infile "&file" lrecl=32767 truncover;
        input _value_ $char32767.;
        call symputx("&macvar", strip(_value_), 'G');
    run;
    %mend;

    %read_secret(/secrets/sasmail_server_client_id,CLIENT_ID);
    %read_secret(/secrets/sasmail_server_client_secret,CLIENT_SECRET);
    %read_secret(/secrets/entra_id_tenant_id,TENANT_ID);

    /*--------------------------------*/
    /* Get OAuth token                */
    /*--------------------------------*/
    filename resp temp;

    proc http
        url="https://login.microsoftonline.com/&TENANT_ID/oauth2/v2.0/token"
        method="POST"
        in="grant_type=client_credentials%str(&)
            client_id=&CLIENT_ID%str(&)
            client_secret=&CLIENT_SECRET%str(&)
            scope=https://graph.microsoft.com/.default"
        ct="application/x-www-form-urlencoded"
        out=resp;
    run;

    libname auth json fileref=resp;

    data _null_;
        set auth.root;
        call symputx('access_token', access_token);
    run;


    %if &has_attach %then %do;

        data _null_;
            length fname $256 ext $10 ctype $100;

            fname = "&attachment";
            attachment_name = scan(fname, -1, '/\');
            ext = lowcase(scan(fname, -1, '.'));

            select (ext);
                when ('csv')   ctype='text/csv';
                when ('txt')   ctype='text/plain';
                when ('zip')   ctype='application/zip';
                when ('doc')   ctype='application/msword';
                when ('docx')  ctype='application/vnd.openxmlformats-officedocument.wordprocessingml.document';
                when ('pdf')   ctype='application/pdf';
                when ('xls')   ctype='application/vnd.ms-excel';
                when ('xlsx')  ctype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
                when ('ppt')   ctype='application/vnd.ms-powerpoint';
                when ('pptx')  ctype='application/vnd.openxmlformats-officedocument.presentationml.presentation';
                when ('html','htm') ctype='text/html';
                otherwise ctype='application/octet-stream';
            end;

            call symputx('content_type', ctype);
            call symputx('attachment_name', attachment_name);
        run;

    %end;

    /*--------------------------------*/
    /* Build JSON with attachment     */
    /*--------------------------------*/
    filename mail temp;

    %if &has_attach %then %do;

        filename src "&attachment" recfm=f lrecl=15000;

        data _null_;
            file mail recfm=n;

            length chunk $15000 b64_chunk $20000 fmt_string $20;

            if _n_ = 1 then do;

                subject_txt = tranwrd(symget('subject'), '"', '\"');
                body_txt    = tranwrd(symget('body'),    '"', '\"');

                put '{'
                    ' "message": {'
                    '  "subject": "' subject_txt '",'
                    '  "body": {'
                    '   "contentType": "HTML",'
                    '   "content": "' body_txt '"'
                    '  },'
                    '  "toRecipients": [';

                
                %do i=1 %to %sysfunc(countw(&to));
                    %if &i>1 %then put ",";
                    put ' { "emailAddress": { "address": "%scan(&to,&i)" } }';
                %end;

                put ']';

               
                %if &has_cc %then %do;
                    put ',"ccRecipients": [';
                    %do j=1 %to %sysfunc(countw(&cc));
                        %if &j>1 %then put ",";
                        put ' { "emailAddress": { "address": "%scan(&cc,&j)" } }';
                    %end;
                    put ']';
                %end;

               
                put ',"attachments":[{'
                    '"@odata.type":"#microsoft.graph.fileAttachment",'
                    '"name":"' "&attachment_name" '",'
                    '"contentType":"' "&content_type" '",'
                    '"contentBytes":"';
            end;

            infile src recfm=f lrecl=15000 end=eof length=bytes_read;

            input chunk $char15000.;

            b64_len = ceil(bytes_read/3)*4;
            fmt_string = cats('$base64x', b64_len);
            b64_chunk = putc(substr(chunk,1,bytes_read), fmt_string);

            put b64_chunk $varying20000. length(b64_chunk) @@;

            if eof then do;
                put '"}]},"saveToSentItems":"true"}';
            end;
        run;

    %end;
    %else %do;

 
        data _null_;
            file mail lrecl=32767;

            subject_txt = tranwrd(symget('subject'), '"', '\"');
            body_txt    = tranwrd(symget('body'),    '"', '\"');

            put '{';
            put ' "message": {';
            put '  "subject": "' subject_txt '",';
            put '  "body": {';
            put '   "contentType": "HTML",';
            put '   "content": "' body_txt '"';
            put '  },';
            put '  "toRecipients": [';

            %do i=1 %to %sysfunc(countw(&to));
                %if &i>1 %then put ",";
                put ' { "emailAddress": { "address": "%scan(&to,&i)" } }';
            %end;

            put ']';

            %if &has_cc %then %do;
                put ',"ccRecipients": [';
                %do j=1 %to %sysfunc(countw(&cc));
                    %if &j>1 %then put ",";
                    put ' { "emailAddress": { "address": "%scan(&cc,&j)" } }';
                %end;
                put ']';
            %end;

            
            put ' },';
            put ' "saveToSentItems": true';
            put '}';

        run;

    %end;

    /*--------------------------------*/
    /* Send email                     */
    /*--------------------------------*/
    filename sendresp temp;

    proc http
        url="https://graph.microsoft.com/v1.0/users/&SENDER_EMAIL/sendMail"
        method="POST"
        in=mail
        out=sendresp;
        headers
            "Authorization"="Bearer &access_token"
            "Content-Type"="application/json";
    run;

    data _null_;
        infile sendresp;
        input;
        putlog _infile_;
    run;

%mend send_mail;

data null;
    format dte ddmmyy10. tme time8.;
    dt = datetime();

call symputx(
    'mail_body',
    catx(
        '',
        '<html><body>',
        '<p>Hello,</p>',
        '<p>The scheduled job completed successfully on ',
        put(datepart(dt), ddmmyy10.),
        ' at ',
        put(timepart(dt), time8.),
        '.</p>',
        '<p>Thank you,<br>',
        'Support Team</p>',
        '</body></html>'
    ),
    'L'
);
run;

%send_mail(
    to=test@org.com org@gmail.com,
    subject=Test SAS Extract Completed Successfully,
    body=%superq(mail_body)
    attachment=/mnt/discovery/ukida/_shared/test14.zip
);

 

gwootton
SAS Super FREQ
I would recommend you start by looking at the generated payload file and making sure it's valid JSON being generated.
For example, change "filename mail temp" to "filename mail /tmp/mail.json" and then open the file with an editor that checks JSON syntax.
--
Greg Wootton | Principal Systems Technical Support Engineer