Hướng dẫn

Google Sheets Quản Lý Email Marketing: Gửi 500 Email Tự Động [Apps Script 2026]

Tuân HoangTuân Hoang
9 tháng 6, 2026
10 phút đọc
Ảnh minh họa bài viết: Google Sheets Quản Lý Email Marketing: Gửi 500 Email Tự Động [Apps Script 2026]

Khi Nào Nên Dùng Google Sheets + Apps Script Thay Vì Mailchimp?

Bạn có danh sách khách hàng trong Google Sheets và cần gửi email marketing thường xuyên — nhưng lại không muốn trả tiền cho Mailchimp, Brevo hay SendGrid mỗi tháng. Tin tốt là: nếu danh sách của bạn dưới 500 người và tần suất gửi hợp lý, bạn hoàn toàn có thể tự xây hệ thống email marketing miễn phí bằng Google Sheets + Apps Script.

Dưới đây là bảng so sánh để bạn quyết định đúng:

Tiêu chí Google Sheets + Apps Script Mailchimp / Brevo
Chi phí Miễn phí (chỉ tốn Workspace nếu có) Từ $13–$20/tháng cho 500 contact
Giới hạn gửi/ngày 500 (Gmail free) / 1.500 (Workspace) Không giới hạn (tùy gói)
Cá nhân hóa sâu từ dữ liệu Rất mạnh — dữ liệu ngay trong Sheets Trung bình — cần import/export
Kiểm soát logic gửi Toàn quyền — code theo ý muốn Hạn chế theo giao diện
Phù hợp cho Danh sách nhỏ, cần personalization sâu, budget = 0 Danh sách lớn, cần analytics chuyên nghiệp

Kết luận: Nếu bạn đang ở giai đoạn đầu, danh sách dưới 500 người, và muốn kiểm soát toàn bộ dữ liệu trong nội bộ — Google Sheets + Apps Script là lựa chọn hoàn hảo. Bài này sẽ hướng dẫn bạn xây từ đầu đến cuối.

Cấu Trúc 4 Sheet Cần Tạo

Toàn bộ hệ thống email marketing được quản lý trong một Google Spreadsheet với 4 sheet riêng biệt. Mỗi sheet có vai trò rõ ràng, giúp code dễ đọc và dữ liệu dễ tra cứu.

Sheet 1: Subscribers (Danh Sách Email)

Đây là trung tâm dữ liệu — nơi lưu thông tin từng subscriber và lịch sử tương tác của họ.

Cột Ý nghĩa Ví dụ
A: EmailĐịa chỉ emailnguyen@example.com
B: TênTên để cá nhân hóaNguyễn Văn A
C: Công tyTên công ty (nếu có)Công ty ABC
D: Phân loạiVIP / Regular / NewVIP
E: Ngày đăng kýTimestamp đăng ký01/01/2026
F: Trạng tháiActive / UnsubscribedActive
G: Lần mở gần nhấtTimestamp lần cuối mở email15/05/2026
H: Số lần mởTổng số lần đã mở email7
I: Ghi chúNote tùy ýKhách VIP Q1 2026

Sheet 2: Templates (Mẫu Email)

Lưu trữ các mẫu email HTML có thể tái sử dụng. Dùng placeholder {{name}}, {{company}}, {{unsubscribe_link}} để cá nhân hóa khi gửi.

Cột Ý nghĩa
A: Tên templateTên định danh (vd: "Chào mừng", "Flash sale")
B: SubjectTiêu đề email — có thể dùng {{name}}
C: Body HTMLNội dung HTML đầy đủ với placeholder
D: Body TextPhiên bản text thuần (fallback cho email client cũ)
E: ActiveTRUE/FALSE — template nào được dùng trong lần gửi tới

Ví dụ placeholder trong Subject: {{name}}, ưu đãi đặc biệt chỉ trong hôm nay!

Placeholder hỗ trợ: {{name}} · {{company}} · {{product}} · {{unsubscribe_link}}

Sheet 3: Campaigns (Chiến Dịch)

Mỗi lần gửi email hàng loạt là một campaign. Sheet này lưu lịch sử và kết quả từng campaign.

Cột Ý nghĩa
A: Tên campaignMô tả ngắn (vd: "Flash Sale Tháng 6")
B: TemplateTên template dùng cho campaign này
C: Đối tượngAll / VIP / New / Regular
D: Ngày gửiTimestamp thực tế gửi
E: Tổng gửiSố email đã gửi thành công
F: Đã mởSố người đã mở email (từ tracking)
G: Open rate= F/E — tính bằng công thức Sheets
H: Trạng tháiPending / Sending / Done / Failed
I: Ghi chúNhận xét sau khi gửi

Sheet 4: Log (Nhật Ký Tracking)

Ghi lại mọi sự kiện: gửi thành công, mở email, click link, unsubscribe. Đây là nguồn dữ liệu để tính open rate và phân tích hành vi.

Cột Giá trị ví dụ
A: Timestamp09/06/2026 08:32:15
B: Emailnguyen@example.com
C: CampaignFlash Sale Tháng 6
D: Loại sự kiệnSent / Opened / Clicked / Unsubscribed / Error

Code Apps Script: Gửi Email Personalized

Đây là hàm cốt lõi — đọc danh sách subscriber, áp dụng template đang active, thay thế placeholder bằng dữ liệu thực và gửi qua GmailApp.

function guiEmailMarketing() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const subscribers = ss.getSheetByName('Subscribers').getDataRange().getValues();
  const templates = ss.getSheetByName('Templates').getDataRange().getValues();
  const log = ss.getSheetByName('Log');

  // Lấy template có cột E = TRUE
  const template = templates.slice(1).find(r => r[4] === true);
  if (!template) {
    Logger.log('Không có template active. Bật cột E = TRUE cho template muốn gửi.');
    return;
  }

  const campaignName = template[0];
  const subject = template[1];
  const bodyHtml = template[2];
  let sentCount = 0;

  subscribers.slice(1).forEach(sub => {
    const email   = sub[0];
    const name    = sub[1] || 'Bạn';
    const company = sub[2] || '';
    const status  = sub[5];

    // Bỏ qua nếu đã unsubscribe hoặc không có email
    if (!email || status === 'Unsubscribed') return;

    // Tạo link unsubscribe cá nhân hóa
    const unsubLink = `https://sheet.com.vn/unsubscribe?email=${encodeURIComponent(email)}`;

    // Merge field: thay thế placeholder bằng dữ liệu thực
    const html = bodyHtml
      .replace(/{{name}}/g, name)
      .replace(/{{company}}/g, company)
      .replace(/{{unsubscribe_link}}/g, unsubLink);

    const personalSubject = subject.replace(/{{name}}/g, name);

    try {
      GmailApp.sendEmail(email, personalSubject, '', {
        htmlBody: html,
        name: 'Sheet.com.vn',
        replyTo: 'support@sheet.com.vn'
      });

      log.appendRow([new Date(), email, campaignName, 'Sent']);
      sentCount++;
      Utilities.sleep(200); // Tránh rate limit Gmail: ~5 email/giây
    } catch(e) {
      log.appendRow([new Date(), email, campaignName, 'Error: ' + e.message]);
    }
  });

  Logger.log(`Hoàn tất! Đã gửi: ${sentCount} email.`);
  SpreadsheetApp.getUi().alert(`Gửi xong! ${sentCount} email đã được gửi.`);
}

Lưu ý quan trọng:

  • Utilities.sleep(200): nghỉ 200ms giữa mỗi email — tránh lỗi "Service invoked too many times". Nếu danh sách lớn, tăng lên 300–500ms.
  • Lần đầu chạy, Apps Script sẽ yêu cầu quyền truy cập Gmail và Sheets — bấm "Allow".
  • Kiểm tra quota còn lại: Extensions → Apps Script → Quotas.

Tracking Mở Email Bằng Pixel Ẩn

Để biết ai đã mở email, kỹ thuật phổ biến nhất là nhúng một ảnh 1×1 pixel vô hình vào HTML. Khi email được mở, trình duyệt/email client tải ảnh đó → Apps Script Web App nhận request và ghi log.

Bước 1: Tạo hàm tạo pixel tracking

// Trả về thẻ <img> ẩn chứa URL tracking
function getTrackingPixel(email, campaignName) {
  // Thay YOUR_DEPLOYMENT_ID bằng Deployment ID thực của Web App
  const trackUrl = 'https://script.google.com/macros/s/YOUR_DEPLOYMENT_ID/exec'
    + '?action=open'
    + '&email=' + encodeURIComponent(email)
    + '&campaign=' + encodeURIComponent(campaignName);

  return '<img src="' + trackUrl + '" width="1" height="1" '
       + 'style="display:none;border:0" alt="" />';
}

Bước 2: Triển khai Web App nhận sự kiện tracking

// Web App endpoint — xử lý request tracking
// Deploy: Extensions → Apps Script → Deploy → New deployment → Web app
// Execute as: Me | Who has access: Anyone
function doGet(e) {
  if (e.parameter.action === 'open') {
    const sheetId = 'YOUR_SPREADSHEET_ID'; // ID trong URL của Spreadsheet
    const logSheet = SpreadsheetApp.openById(sheetId).getSheetByName('Log');

    logSheet.appendRow([
      new Date(),
      e.parameter.email || '',
      e.parameter.campaign || '',
      'Opened'
    ]);
  }

  // Trả về ảnh 1x1 pixel GIF trong suốt
  return ContentService
    .createTextOutput('')
    .setMimeType(ContentService.MimeType.TEXT);
}

// Gọi hàm này để tự động cập nhật số lần mở vào cột H của Subscribers
function syncOpenCountToSubscribers() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const logData = ss.getSheetByName('Log').getDataRange().getValues();
  const subSheet = ss.getSheetByName('Subscribers');
  const subData = subSheet.getDataRange().getValues();

  // Đếm số lần "Opened" theo từng email
  const openCounts = {};
  const lastOpen = {};
  logData.slice(1).forEach(row => {
    const email = row[1];
    const type = row[3];
    if (type === 'Opened') {
      openCounts[email] = (openCounts[email] || 0) + 1;
      if (!lastOpen[email] || row[0] > lastOpen[email]) {
        lastOpen[email] = row[0];
      }
    }
  });

  // Cập nhật cột G (lần mở gần nhất) và H (số lần mở)
  subData.slice(1).forEach((sub, idx) => {
    const email = sub[0];
    if (openCounts[email]) {
      const rowNum = idx + 2; // +2 vì slice(1) và header
      subSheet.getRange(rowNum, 7).setValue(lastOpen[email]); // cột G
      subSheet.getRange(rowNum, 8).setValue(openCounts[email]); // cột H
    }
  });

  Logger.log('Đã sync open count xong!');
}

Cách deploy Web App:

  1. Trong Apps Script Editor: Deploy → New deployment
  2. Type: Web app
  3. Execute as: Me
  4. Who has access: Anyone (để email client có thể gọi tracking)
  5. Copy Deployment ID và thay vào YOUR_DEPLOYMENT_ID trong hàm getTrackingPixel

Lưu ý về tracking pixel: Một số email client (đặc biệt Apple Mail với tính năng Mail Privacy Protection) chặn tracking pixel. Open rate thực tế có thể thấp hơn 20–30% so với con số hiển thị — đây là hạn chế chung của mọi công cụ email marketing, không riêng giải pháp này.

Xử Lý Unsubscribe: Yêu Cầu Pháp Lý Bắt Buộc

Luật GDPR (EU), CAN-SPAM (US) và nhiều quy định khác bắt buộc mỗi email marketing phải có link unsubscribe. Nếu không có, bạn có thể bị báo cáo spam và tài khoản Gmail bị hạn chế.

Bước 1: Nhúng link unsubscribe vào template HTML

<!-- Thêm vào cuối mỗi template email -->
<p style="text-align:center; color:#999; font-size:12px; margin-top:30px;">
  Bạn nhận email này vì đã đăng ký nhận tin từ Sheet.com.vn.
  <br>
  <a href="{{unsubscribe_link}}" style="color:#999;">Hủy đăng ký nhận tin</a>
</p>

Bước 2: Xây endpoint xử lý unsubscribe

// Thêm vào hàm doGet() đã tạo ở trên
function doGet(e) {
  const action = e.parameter.action;

  if (action === 'open') {
    // ... xử lý tracking như đã viết ...
  }

  if (action === 'unsubscribe') {
    const email = e.parameter.email;
    if (email) {
      updateSubscriberStatus(email, 'Unsubscribed');
    }
    // Trả về trang xác nhận đơn giản
    return HtmlService.createHtmlOutput(
      '<h2>Bạn đã hủy đăng ký thành công.</h2>'
      + '<p>Chúng tôi sẽ không gửi email cho bạn nữa.</p>'
    );
  }

  return ContentService.createTextOutput('').setMimeType(ContentService.MimeType.TEXT);
}

// Hàm cập nhật trạng thái trong Subscribers sheet
function updateSubscriberStatus(email, newStatus) {
  const sheet = SpreadsheetApp.openById('YOUR_SPREADSHEET_ID')
                              .getSheetByName('Subscribers');
  const data = sheet.getDataRange().getValues();

  data.forEach((row, idx) => {
    if (row[0] === email) {
      sheet.getRange(idx + 1, 6).setValue(newStatus); // Cột F: Trạng thái
      Logger.log('Đã cập nhật: ' + email + ' → ' + newStatus);
    }
  });
}

Sau khi subscriber click link unsubscribe: trạng thái trong Sheet tự động chuyển sang "Unsubscribed", và hàm guiEmailMarketing() sẽ bỏ qua họ trong mọi lần gửi tiếp theo nhờ điều kiện if (status === 'Unsubscribed') return.

Segmentation: Gửi Email Theo Nhóm Khách Hàng

Thay vì gửi đồng loạt cho mọi người, bạn có thể lọc và gửi cho từng phân khúc. Điều này tăng open rate đáng kể vì nội dung phù hợp hơn với từng nhóm.

function guiEmailTheoNhom(nhom) {
  // nhom nhận giá trị: 'VIP', 'Regular', 'New', hoặc 'All'
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const allSubs = ss.getSheetByName('Subscribers').getDataRange().getValues().slice(1);

  let targets;
  if (nhom === 'All') {
    targets = allSubs.filter(r => r[5] === 'Active' && r[0]);
  } else {
    targets = allSubs.filter(r => r[3] === nhom && r[5] === 'Active' && r[0]);
  }

  Logger.log('Sẽ gửi cho ' + targets.length + ' subscriber thuộc nhóm: ' + nhom);

  // ... phần còn lại giống hàm guiEmailMarketing() ...
}

// Ví dụ gọi hàm:
// guiEmailTheoNhom('VIP')    → chỉ gửi cho VIP
// guiEmailTheoNhom('New')    → onboarding cho người mới
// guiEmailTheoNhom('All')    → gửi cho tất cả đang Active

Ý tưởng phân khúc thực tế

  • VIP: Early access, ưu đãi độc quyền, nội dung cao cấp
  • New: Chuỗi email onboarding 3–5 email tự động (welcome → hướng dẫn → case study)
  • Regular: Newsletter thông thường, update sản phẩm
  • Inactive (mở < 1 lần/tháng): Email re-engagement "Bạn có muốn tiếp tục nhận tin?"

Best Practices: Gửi Email Marketing Hiệu Quả

Subject Line — Yếu Tố Quyết Định Open Rate

  • Giữ dưới 50 ký tự (hiển thị đầy đủ trên mobile)
  • Dùng personalization: "{{name}}, ưu đãi flash sale chỉ còn 2 giờ!"
  • Tránh từ khóa spam: "MIỄN PHÍ", "CLICK NGAY", quá nhiều dấu chấm than
  • Thử A/B: gửi 2 subject khác nhau cho 2 nhóm nhỏ, subject nào tốt hơn thì dùng cho phần còn lại

Thời Điểm Gửi Tối Ưu

Thời điểm Open rate trung bình Lý do
Thứ 3–4, 8–10h sángCao nhất (~25–28%)Đầu tuần làm việc, check email buổi sáng
Thứ 5, 12–13h trưaKhá tốt (~20–22%)Giờ nghỉ trưa lướt email
Thứ 6 chiều, Cuối tuầnThấp nhất (<15%)Mọi người giảm check email

Cài schedule gửi tự động:

// Đặt trigger tự động chạy hàng tuần
function caiTriggerGuiEmail() {
  // Xóa trigger cũ (nếu có)
  ScriptApp.getProjectTriggers().forEach(t => {
    if (t.getHandlerFunction() === 'guiEmailMarketing') {
      ScriptApp.deleteTrigger(t);
    }
  });

  // Tạo trigger mới: chạy thứ 3 hàng tuần lúc 8–9h sáng
  ScriptApp.newTrigger('guiEmailMarketing')
    .timeBased()
    .onWeekDay(ScriptApp.WeekDay.TUESDAY)
    .atHour(8)
    .create();

  Logger.log('Đã cài trigger: gửi email mỗi thứ 3 lúc 8–9h sáng');
}

Quy Tắc Vàng Về Tần Suất

  • Tối đa 2 lần/tuần — gửi nhiều hơn dễ bị đánh dấu spam
  • Luôn có unsubscribe link trong mọi email
  • Test trước khi gửi hàng loạt: Gửi cho chính bạn, kiểm tra hiển thị trên điện thoại và PC
  • Warm up dần: Tuần đầu gửi 50 email → tăng dần lên 200 → 500, tránh bị Gmail đánh dấu là spam account

Dashboard Tracking: Đo Hiệu Quả Chiến Dịch

Sau khi gửi và thu thập dữ liệu tracking, bạn có thể xây một dashboard đơn giản ngay trong Sheets.

Các Công Thức Cần Thiết (Sheet: Campaigns)

// Cột G: Open rate = Đã mở / Tổng gửi
=IFERROR(F2/E2, 0)  → format ô dạng %

// Tổng open rate toàn bộ campaign
=SUMIF(H:H,"Done",F:F) / SUMIF(H:H,"Done",E:E)

// Subscriber đang active (đếm từ Subscribers sheet)
=COUNTIF(Subscribers!F:F,"Active")

// Tỷ lệ unsubscribe
=COUNTIF(Subscribers!F:F,"Unsubscribed") / COUNTA(Subscribers!A:A) - 1

Benchmark Tham Khảo

Chỉ số Trung bình ngành Tốt Xuất sắc
Open rate18–22%25–30%>35%
Click rate2–3%4–6%>8%
Unsubscribe rate<0.5%<0.2%<0.1%
Bounce rate (error)<2%<1%<0.5%

Tạo biểu đồ open rate theo campaign: Chọn cột "Tên campaign" và "Open rate" → Insert → Chart → Column chart. Điều chỉnh màu sắc để dễ nhìn. Dashboard đơn giản nhưng đủ để ra quyết định.

Giới Hạn Gmail Cần Biết Trước Khi Dùng

Đây là giới hạn cứng của Google — bạn không thể vượt qua dù muốn:

Loại tài khoản Giới hạn gửi/ngày Giới hạn recipient/email
Gmail cá nhân (free)500 email/ngày100 địa chỉ/email
Google Workspace (trả phí)1.500 email/ngày2.000 địa chỉ/email
Apps Script (qua GmailApp)Tính vào quota trênKhông thêm

Khi vượt giới hạn: GmailApp.sendEmail() sẽ ném Exception. Code đã xử lý lỗi này trong try/catch và ghi vào Log với trạng thái "Error". Kiểm tra Log để biết email nào chưa được gửi và gửi bù hôm sau.

Nếu danh sách vượt 500 người:

  • Nâng cấp lên Google Workspace (từ $6/tháng) → 1.500/ngày
  • Hoặc chia campaign thành nhiều ngày (ví dụ: A–M hôm nay, N–Z ngày mai)
  • Hoặc chuyển sang Brevo free tier (300 email/ngày, không giới hạn contact)

Kết Luận

Với Google Sheets + Apps Script, bạn có một hệ thống email marketing đầy đủ tính năng hoàn toàn miễn phí: gửi email personalized, tracking mở email, xử lý unsubscribe tự động, phân khúc theo nhóm, và dashboard theo dõi hiệu quả.

Đây là giải pháp lý tưởng cho startups, freelancer, và SMB ở giai đoạn đầu — khi mỗi đồng ngân sách đều quan trọng và bạn cần kiểm soát dữ liệu khách hàng trong nội bộ.

Lộ trình triển khai gợi ý:

  1. Tuần 1: Tạo 4 sheet, nhập dữ liệu subscriber, viết template email đầu tiên
  2. Tuần 2: Cài code gửi email, test với 5–10 địa chỉ email của bạn
  3. Tuần 3: Deploy Web App tracking, thêm pixel vào template
  4. Tuần 4: Gửi campaign đầu tiên, đọc kết quả, tối ưu subject line

Khi danh sách của bạn vượt 1.000+ người hoặc cần automation phức tạp hơn (A/B testing, email sequence tự động theo hành vi), đó là lúc nên cân nhắc chuyển sang Brevo hoặc Mailchimp. Nhưng đến lúc đó, bạn đã có đủ dữ liệu và kinh nghiệm để ra quyết định sáng suốt hơn.

Tài liệu tham khảo: Google Apps Script — GmailApp Reference | Gmail sending limits

Chia sẻ bài viết:

Tuân Hoang

Tuân Hoang

Đội ngũ SheetStore

Google SheetsGoogle Apps ScriptCRMAutomationPhần mềm quản lý doanh nghiệp

Google Workspace Certified, 5+ years experience

Bạn thấy bài viết hữu ích?

Đăng ký nhận thông báo khi có bài viết mới.

Nhận thông báo khi có bài viết mới. Không spam, hứa luôn! 😊

Bình luận (0)

Vui lòng đăng nhập để tham gia thảo luận