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;
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;
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:
%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
);
Nearly 200 sessions are now available on demand in the Innovate Hub.
Watch Now →