Google Sheets Quản Lý Email Marketing: Gửi 500 Email Tự Động [Apps Script 2026]
![Ảnh minh họa bài viết: Google Sheets Quản Lý Email Marketing: Gửi 500 Email Tự Động [Apps Script 2026]](/og-image.jpg)
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ỉ email | nguyen@example.com |
| B: Tên | Tên để cá nhân hóa | Nguyễn Văn A |
| C: Công ty | Tên công ty (nếu có) | Công ty ABC |
| D: Phân loại | VIP / Regular / New | VIP |
| E: Ngày đăng ký | Timestamp đăng ký | 01/01/2026 |
| F: Trạng thái | Active / Unsubscribed | Active |
| G: Lần mở gần nhất | Timestamp lần cuối mở email | 15/05/2026 |
| H: Số lần mở | Tổng số lần đã mở email | 7 |
| 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 template | Tên định danh (vd: "Chào mừng", "Flash sale") |
| B: Subject | Tiêu đề email — có thể dùng {{name}} |
| C: Body HTML | Nội dung HTML đầy đủ với placeholder |
| D: Body Text | Phiên bản text thuần (fallback cho email client cũ) |
| E: Active | TRUE/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 campaign | Mô tả ngắn (vd: "Flash Sale Tháng 6") |
| B: Template | Tên template dùng cho campaign này |
| C: Đối tượng | All / VIP / New / Regular |
| D: Ngày gửi | Timestamp thực tế gửi |
| E: Tổng gửi | Số 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ái | Pending / 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: Timestamp | 09/06/2026 08:32:15 |
| B: Email | nguyen@example.com |
| C: Campaign | Flash Sale Tháng 6 |
| D: Loại sự kiện | Sent / 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:
- Trong Apps Script Editor: Deploy → New deployment
- Type: Web app
- Execute as: Me
- Who has access: Anyone (để email client có thể gọi tracking)
- Copy Deployment ID và thay vào
YOUR_DEPLOYMENT_IDtrong hàmgetTrackingPixel
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áng | Cao nhất (~25–28%) | Đầu tuần làm việc, check email buổi sáng |
| Thứ 5, 12–13h trưa | Khá tốt (~20–22%) | Giờ nghỉ trưa lướt email |
| Thứ 6 chiều, Cuối tuần | Thấ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 rate | 18–22% | 25–30% | >35% |
| Click rate | 2–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ày | 100 địa chỉ/email |
| Google Workspace (trả phí) | 1.500 email/ngày | 2.000 địa chỉ/email |
| Apps Script (qua GmailApp) | Tính vào quota trên | Khô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 ý:
- Tuần 1: Tạo 4 sheet, nhập dữ liệu subscriber, viết template email đầu tiên
- Tuần 2: Cài code gửi email, test với 5–10 địa chỉ email của bạn
- Tuần 3: Deploy Web App tracking, thêm pixel vào template
- 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
Đội ngũ SheetStore
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.

