Xử lý Files bất đồng bộ và sử dụng JavaScript gửi email qua Outlook REST API

Ở những bài trước mình có giới thiệu về Outook REST API cũng như sử dụng Ruby on Rails để thao tác các tác vụ cơ bản như gửi và nhận mail. Bạn có thể xem ở https://viblo.asia/nguyentrunghieu/posts/3NVRkb3zv9xn

Trong bài này, mình sẽ trình bày các bước để gửi một email kèm files bằng JavaScript sử dụng Outlook REST API.

Mã hóa files theo chuẩn Base64

Tại sao cần mã hoá theo chuẩn Base64 Files có thể ở bất cứ định dạng nào, nội dung bên trong có thể chưa bất cứ loại ký tự nào, rất có thể sẽ chứa những ký tự điều khiển hoặc ký tự đặc biệt ảnh hưởng tới việc toàn vẹn dữ liệu. Chuẩn Base64 là tập hợp các ký tự từ A-Z, a-z, 0-9, +/
Chi tiết về Base64 cũng như các quy tắc, cách mã hóa mình sẽ đề cập ở bài sau.
Quay lại với việc mã hóa files theo chuẩn Base64 bằng JavaScript, đầu tiên ta cần đọc file dưới dạng nhị phân sau đó chuyển sang Base64.

function encode(file, callback) {
  var reader = new FileReader();
  reader.onload = function(readerEvt) {
    var binaryString = readerEvt.target.result;
    callback(btoa(binaryString));
  }
  reader.readAsBinaryString(file);
}

Đây là 1 tác vụ bất đồng bộ, nên ta cần phải cẩn trọng khi sử dụng nó. Để quản lý các task bất đồng bộ có nhiều cách, ở đây mình sẽ sử dụng Promise.

var promisesArray = files.map(function(file) {
  var promise = new Promise(function(resolve, reject) {
    encode(file, function(bytes) {
      resolve([file.name, bytes]);
    });
  });
  return promise;
});
return Promise.all(promisesArray);

Đoạn mã trên sẽ trả về 1 mảng các Promise, mình sẽ sử dung ở bên dưới.

Khởi tạo cấc trúc email

Nội dung email sẽ được thể hiện qua JSON để đưa vào payload đi kèm với request, đây là cấc trúc email gồm các thành phần cơ bản :

{
  "Message": {
    "Subject": subject,
    "Body": {
      "ContentType": "HTML",
      "Content": content
    },
    "ToRecipients": [],
    "CcRecipients": [],
    "Attachments": []
  }
};

Đây là hàm khởi tạo nội dung email hoàn chỉnh kết hợp với mã hóa files

function generate_message(subject, content, to_emails, cc_emails, files) {
  result = {
    "Message": {
      "Subject": subject,
      "Body": {
        "ContentType": "HTML",
        "Content": content
      },
      "ToRecipients": [],
      "CcRecipients": [],
      "Attachments": []
    }
  };
  if (to_emails) {
    $.each(to_emails, function(key, to) {
      recipient = {
        "EmailAddress": {
          "Address": to
        }
      }
      result.Message.ToRecipients.push(recipient);
    });
  }
  if (cc_emails) {
    $.each(cc_emails, function(key, cc) {
      recipient = {
        "EmailAddress": {
          "Address": cc
        }
      }
      result.Message.CcRecipients.push(recipient);
    });
  }

  var promisesArray = files.map(function(file) {
    var promise = new Promise(function(resolve, reject) {
      encode(file, function(bytes) {
        resolve([file.name, bytes]);
      });
    });
    return promise;
  });
  return Promise.all(promisesArray);
}

Tham số truyền vào bao gồm subject : Tiêu đề, content : Nội dung, to_emails : Mảng các địa chỉ cần gửi, cc_emails : Mảng các địa chỉ cần CC tới, và files : Danh sách các file muốn đính kèm.

Khởi tạo request gửi email

Ta sẽ sử dụng Jquery AJAX để gửi request, cụ thể như code bên dưới :

function send_request(payload, repeatable, token) {
  var status;
  $.ajax({
    method: "POST",
    url: "https://outlook.office.com/api/v1.0/me/sendmail",
    async: false,
    headers: {
      "Authorization": "Bearer " + token,
      "content-type": "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8"
    },
    data: JSON.stringify(payload),
    complete: function(request, textStatus) {
      status = textStatus;
    }
  });
  if ((status === "error") && repeatable) {
    status = send_request(payload, false, get_new_token());
  }
  return status;
}

Tham số repeatable để quyết định việc reset lại token và thử lại việc gửi mail một lần nữa. Hàm get_new_token() sử dụng để reset lại token khi token hiện tại hết hạn. Về cơ bản hàm này sẽ gửi request POST lên https://login.microsoftonline.com/common/oauth2/token
với header Content-Type: application/x-www-form-urlencoded, body sẽ bao gồm cấu trúc
grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN&scope=YOUR_SCOPE&client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET
Response trả về sẽ bao gồm 1 cặp access_tokenrefresh_token mới. Chi tiết hàm này để bạn đọc tự viết.

Code mẫu

Dưới đây là code hoàn chỉnh để các bạn tham khảo:

function encode(file, callback) {
  var reader = new FileReader();
  reader.onload = function(readerEvt) {
    var binaryString = readerEvt.target.result;
    callback(btoa(binaryString));
  }
  reader.readAsBinaryString(file);
}

function generate_message(subject, content, to_emails, cc_emails, files) {
  result = {
    "Message": {
      "Subject": subject,
      "Body": {
        "ContentType": "HTML",
        "Content": content
      },
      "ToRecipients": [],
      "CcRecipients": [],
      "Attachments": []
    }
  };
  if (to_emails) {
    $.each(to_emails, function(key, to) {
      recipient = {
        "EmailAddress": {
          "Address": to
        }
      }
      result.Message.ToRecipients.push(recipient);
    });
  }
  if (cc_emails) {
    $.each(cc_emails, function(key, cc) {
      recipient = {
        "EmailAddress": {
          "Address": cc
        }
      }
      result.Message.CcRecipients.push(recipient);
    });
  }

  var promisesArray = files.map(function(file) {
    var promise = new Promise(function(resolve, reject) {
      encode(file, function(bytes) {
        resolve([file.name, bytes]);
      });
    });
    return promise;
  });
  return Promise.all(promisesArray);
}

function send_request(payload, repeatable, token) {
  var status;
  $.ajax({
    method: "POST",
    url: "https://outlook.office.com/api/v1.0/me/sendmail",
    async: false,
    headers: {
      "Authorization": "Bearer " + token,
      "content-type": "application/json;odata.metadata=minimal;odata.streaming=true;IEEE754Compatible=false;charset=utf-8"
    },
    data: JSON.stringify(payload),
    complete: function(request, textStatus) {
      status = textStatus;
    }
  });
  if ((status === "error") && repeatable) {
    status = send_request(payload, false, get_new_token());
  }
  return status;
}

function send_message(subject, content, to_recipients, cc_recipients, files, token, callback) {
  generate_message(subject, content, to_recipients, cc_recipients, files).then(function(values) {
    result.Message.Attachments = values.map(function(encoded_attachment) {
      return {
        "@odata.type": "#Microsoft.OutlookServices.FileAttachment",
        "Name": encoded_attachment[0],
        "ContentBytes": encoded_attachment[1]
      }
    });
    callback(send_request(result, true, token));
  });
}

Kết luận

Gửi trực tiếp email sử dụng JavaScipt là một cách hay thay vì thông qua controller như việc gửi mail bằng Ruby On Rails tuy nhiên nó lại tỏ ra không được hay khi các file muốn gửi kèm nằm trên server của mình hoặc chúng ta cần tạo log cho việc gửi mail.
Nội dung cũng như code tham khảo bên trên chắc hẳn còn sai sót do kiến thức của mình còn hạn chế, rất mong các bạn đóng góp ý kiến giúp mình tiến bộ cũng như cải thiện chất lượng bài viết.