Содержание
Три штатных способа и чем каждый ограничен
Подготовка таблицы и код макроса
Какие есть риски использования скрытой копии
Проблемы, с которыми я столкнулся
Ограничения решения и что с ними делать
Иногда в жизни интернет-маркетолога появляются нетривиальные задачи, которые кажутся простыми на первый взгляд. Есть база примерно на 600 почтовых адресов в Excel и нужно разослать им коммерческое письмо/предложение. Сервиса рассылок нет и не будет, пока проведешь тендер, заключишь договор, оплатишь, передашь базу подрядчику, ответишь вопросы от службы безопасности и так далее. Ставить надстройки на рабочую машину нельзя. А рассылку делать надо. Остались штатный Excel и штатный корпоративный Outlook.
Из этого собралось решение. VBA макрос собирает адреса из видимой части таблицы, кладёт их в скрытую копию, подставляет одно из двух свёрстанных HTML-писем и открывает окно нового сообщения. Запуск скрипта выведен прямо в таблицу по кнопке. А отправку делает человек, после проверки в почтовом клиенте.
А потом первая партия ушла и вернулась. Наш Exchange разрешает максимум 50 получателей на письмо, и узнал я об этом из ответного письма, после первой боевой отправки. Поэтому далее по тексту будут встречаться ограничения, вызванные именно политикой безопасности компании, в которой я работаю, а не архитектурными соображениями (партии, паузы, график).
Дальше код целиком, история с лимитом, разбор того, чего я боялся зря, и проблемы корпоративного Outlook.
Три штатных способа и чем каждый ограничен
Начну не с кода, а с выбора способа. Если погуглить как сделать рассылку с помощью Excel, то три четверти материалов по этому запросу предложат вам именно такие варианты:
Слияние Word + Excel
Первое, что советуют все руководства. Вкладка «Рассылки» → «Начать слияние» → мастер, подключаете таблицу, выбираете столбец с адресами, письма падают в «Исходящие» Outlook.
Способ живой и для многих задач лучший. Он единственный из трёх даёт настоящую персонализацию: обращение по имени, название компании, любые поля из таблицы, даже условные правила через IF…THEN…ELSE.
Но в документации Microsoft есть примечание, которого нет ни в одном русскоязычном гайде:
Word отправляет отдельное сообщение на каждый адрес электронной почты. Вы не можете cc или BCC других получателей. Вы не можете добавлять вложения, но вы можете включить ссылки.
Коммерческое предложение PDF-файлом через слияние не отправить в принципе, только ссылкой. И каждый адрес – отдельное письмо, со всеми последствиями для лимитов.
И еще нужен MAPI-совместимый почтовый клиент, а почтовые индексы в источнике данных надо форматировать как текст, иначе Excel съест ведущие нули.
В моем случае не было выбора. Корпоративная безопасность закрыла этот способ, но если у вас он открыт, то начните с него. Это самый простой, понятный и быстрый способ сделать рассылку из эксель.
Макрос с программной отправкой
Другой способ – макрос перебирает строки листа и на каждый адрес вызывает .Send.
With objMail .To = sTo .Subject = sSubject .Body = sBody .SendEnd With
Пишется за десять минут, умеет и вложения, и персонализацию. Минус в том, что письмо уходит без предпросмотра и на каждый адрес расходуется отдельное сообщение. Что для меня тоже не подошло, но вам может пригодиться.
Макрос, который только открывает письмо
То, что собрал я. Макрос набирает партию адресов из видимых строк таблицы, складывает их в BCC (скрытую копию) одного письма, подставляет HTML-тело и вызывает .Display вместо .Send. Дальше менеджер глазами проверяет письмо, выбирает ящик отправителя, при желании цепляет вложение и жмёт «Отправить» руками.
Как выбрать (сравнительная таблица возможностей Excel)
|
|
Слияние Word + Excel |
Макрос с |
Макрос с |
|---|---|---|---|
|
Персонализация по имени |
да |
да |
нет |
|
Вложения |
нет |
да |
да |
|
Просмотр перед отправкой |
да, через автономный режим |
нет |
да |
|
Отправлений на 500 адресов |
500 |
500 |
13 партий |
|
Выбор ящика отправителя |
по умолчанию |
в коде |
у человека |
|
Статистика открытий |
нет |
нет |
нет |
|
Сложность |
низкая |
низкая |
средняя |
Если коротко: нужны имена в письме – слияние. Нужны вложения и не нужен просмотр – .Send. Нужны вложения, просмотр и минимум отправлений – вариант ниже.
Ограничения, с которыми я столкнулся.
Макрос отработал штатно. Письмо ушло. Через минуту в почтовом клиенте появилось уведомление, что письма не отправлены: лимит 50 получателей.
Не окно с ошибкой в VBA, не предупреждение при отправке – обычное ответное письмо. Прочитал его я уже после того, как закрыл Excel и пошёл заниматься другими делами.
У нас локальный Exchange, а не онлайн версия и не Microsoft 365, и лимиты в нём задаёт администратор. Экспериментально выяснять их – плохой способ, но у меня получился именно он.
Что лимит делает с размером партии
В лимит получателей попадают все поля адресатов сразу: «Кому», «Копия» и «Скрытая копия». Значит, считать надо не строки таблицы, а фактические адреса. А у меня у части заказчиков по два адреса – общий info@ и личный адрес контактного лица и оба уходят в скрытую копию.
Плюс два контрольных ящика, которые макрос добавляет к каждой партии (были сделаны для проверки работы менеджера, когда он делает рассылку письмо автоматически дублируется заинтересованным лицам). Они тоже получатели, они тоже в лимите. Про них забыть проще всего.
Итоговый график получился такой:
-
25 строк на партию – это примерно 40 фактических адресов;
-
плюс контрольные – около 42;
-
две партии в день, утром и после обеда;
-
около 500 адресов за неделю с небольшим.
Запас в 8 получателей до лимита выглядит паранойей. В базе число адресов на строку заранее не известно, и партия из 25 строк может оказаться и на 35 адресов, и на 48. Макрос показывает точный счётчик перед отправкой, но лучше, чтобы даже плохой случай не упирался в потолок.
Паузы между партиями я держал не ради лимита в минуту, а ради того, чтобы всплеск исходящего трафика не выглядел для фильтров как компрометация ящика. Разовая рассылка на несколько тысяч внешних адресов за час – известный способ увести корпоративный домен в спам-листы. Восстанавливать репутацию домена дольше, чем согласовывать сервис рассылок.
Если у вас Exchange Online
Мои 50 – это настройка нашего администратора, переносить её на свою среду нельзя. Для Microsoft 365 цифры опубликованы:
|
Лимит |
Значение по умолчанию |
|---|---|
|
Получателей в сутки на ящик |
10 000, скользящее окно 24 часа |
|
Получателей в одном сообщении |
до 1000, настраивается администратором |
|
Сообщений в минуту |
30 |
|
Внешних получателей в сутки |
2000 – стало с апреля 2026 |
Ключевая деталь, из-за которой ломаются интуитивные расчёты. Считаются получатели, а не письма, и считаются повторно. В документации это сформулировано так: 100 писем одним и тем же пяти внешним адресатам засчитываются как 500 внешних получателей.
И вторая деталь. Партия в BCC не экономит лимит получателей. Она экономит число отправлений, время менеджера и нервы, но 500 адресов остаются 500 получателями, как их ни складывай.
Для локального Exchange Server лимитов получателей по умолчанию нет вообще, их целиком задаёт администратор. Для российских почтовых провайдеров сопоставимой публичной документации я не нашёл поэтому если будете делать что-то подобное самостоятельно спросите у вашей поддержки, не переносите цифры Microsoft, я их привел для справки.
Подготовка таблицы и код макроса
Ключевое отличие от типовых гайдов – макрос ничего не отправляет сам.
Он собирает адреса из видимой части таблицы, складывает их в скрытую копию, подставляет выбранное HTML-письмо и открывает окно нового сообщения. Дальше человек проверяет письмо, выбирает нужный ящик отправителя, при желании цепляет вложение и жмёт «Отправить».
Что это даёт:
-
визуальный контроль каждой партии перед уходом;
-
одно письмо на партию вместо сорока – по числу отправлений это одно действие, и адреса не видят друг друга;
-
выбор аккаунта отправителя остаётся за человеком, а не зашит в код;
-
вопрос программного доступа не возникает вовсе, независимо от политик.
После закрытия окна почтового клиента макрос спрашивает «письмо отправлено?»
Если да – проставляет отметку и дату по каждой строке партии.
Если нет – не помечает ничего, и эти адреса попадут в следующий заход.
Про слабое место этой схемы – в разделе про ограничения.
Часть 1. Подготовка таблицы
Требования минимальные: одна строка заголовков, никаких объединённых ячеек, адреса в отдельных столбцах. Моя структура: A – повод (откуда контакт), B – регион, D – заказчик, E – ИНН, F и G – два адреса, J – статус рассылки, K – дата отправки.
Столбцы J и K макрос создаёт сам при первом запуске. Это и есть маркер, который не даёт разослать одно и то же дважды.
Единицей рассылки я сделал строку-заказчика, а не отдельный адрес. Оба email уходят в скрытую копию, но помечается вся строка. Считать удобнее по компаниям, а лимит контролировать по счётчику – макрос показывает его перед отправкой.
Книгу нужно сохранить как .xlsm. В обычном .xlsx макросы не живут.
Часть 2. Основной макрос рассылки
Код целиком. Ниже разберу неочевидные места.
Option Explicit'================== НАСТРОЙКИ ==================Private Const SHEET_NAME As String = "Общая"Private Const FIRST_DATA_ROW As Long = 2Private Const COL_EMAIL1 As Long = 6Private Const COL_EMAIL2 As Long = 7Private Const COL_STATUS As Long = 10Private Const COL_DATE As Long = 11Private Const SEND_TO_EMAIL2 As Boolean = TruePrivate Const DEFAULT_BATCH As Long = 50Private Const TO_ADDRESS As String = ""' Контрольные адреса — всегда идут в скрытую копиюPrivate Const CONTROL_BCC As String = "control1@company.ru; control2@company.ru"' Логотип общий для всех шаблоновPrivate Const LOGO_PATH As String = "C:\Рассылка\logo.png"'=============================================' Список шаблонов: имя / путь к HTML / тема письмаPrivate Sub GetTemplates(names() As String, htmls() As String, subjects() As String) ReDim names(1 To 2) ReDim htmls(1 To 2) ReDim subjects(1 To 2) names(1) = "Имя первого шаблона" htmls(1) = "C:\Рассылка\email_1.html" subjects(1) = "Тема письма" names(2) = "Имя второго шаблона" htmls(2) = "C:\Рассылка\email_2.html" subjects(2) = "Тема письма"End SubPrivate Function PickTemplate(ByRef htmlPath As String, ByRef subj As String, _ ByRef tplName As String) As Boolean Dim names() As String, htmls() As String, subjects() As String GetTemplates names, htmls, subjects Dim i As Long, menu As String For i = LBound(names) To UBound(names) menu = menu & i & " — " & names(i) & vbCrLf Next i Dim ans As String ans = InputBox("Выберите шаблон письма — введите номер:" & vbCrLf & vbCrLf & menu, _ "Выбор шаблона", "1") If ans = "" Then PickTemplate = False: Exit Function If Not IsNumeric(ans) Then MsgBox "Нужен номер шаблона.", vbExclamation: _ PickTemplate = False: Exit Function Dim k As Long: k = CLng(ans) If k < LBound(names) Or k > UBound(names) Then MsgBox "Нет шаблона с номером " & k & ".", vbExclamation PickTemplate = False: Exit Function End If htmlPath = htmls(k): subj = subjects(k): tplName = names(k) PickTemplate = TrueEnd FunctionPublic Sub Рассылка_Outlook() Dim ws As Worksheet On Error Resume Next Set ws = ThisWorkbook.Worksheets(SHEET_NAME) On Error GoTo 0 If ws Is Nothing Then MsgBox "Не найден лист.", vbCritical: Exit Sub Dim htmlPath As String, subj As String, tplName As String If Not PickTemplate(htmlPath, subj, tplName) Then Exit Sub If Dir(htmlPath) = "" Then MsgBox "Не найден файл письма.", vbCritical: Exit Sub If Dir(LOGO_PATH) = "" Then MsgBox "Не найден логотип.", vbCritical: Exit Sub EnsureHeaders ws Dim ans As String, batchN As Long ans = InputBox("Шаблон: " & tplName & vbCrLf & vbCrLf & _ "Сколько получателей (строк) добавить в рассылку?", _ "Рассылка", CStr(DEFAULT_BATCH)) If ans = "" Then Exit Sub If Not IsNumeric(ans) Then Exit Sub batchN = CLng(ans) If batchN <= 0 Then Exit Sub Dim re As Object: Set re = CreateObject("VBScript.RegExp") re.Pattern = "^[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}$" re.IgnoreCase = True Dim seen As Object: Set seen = CreateObject("Scripting.Dictionary") seen.CompareMode = 1 Dim sentRows As New Collection, badRows As New Collection Dim bcc As String, bccCount As Long Dim lastRow As Long, r As Long lastRow = ws.Cells(ws.Rows.Count, COL_EMAIL1).End(xlUp).Row For r = FIRST_DATA_ROW To lastRow If batchN <= 0 Then Exit For ' только видимые строки (учёт фильтра) и только непомеченные If Not ws.Rows(r).Hidden And Len(Trim$(CStr(ws.Cells(r, COL_STATUS).Value))) = 0 Then Dim e1 As String, e2 As String, added As Boolean e1 = Trim$(CStr(ws.Cells(r, COL_EMAIL1).Value)) e2 = Trim$(CStr(ws.Cells(r, COL_EMAIL2).Value)) added = False If re.Test(e1) Then If Not seen.Exists(e1) Then _ seen.Add e1, 1: bcc = bcc & e1 & "; ": bccCount = bccCount + 1 added = True End If If SEND_TO_EMAIL2 And re.Test(e2) Then If Not seen.Exists(e2) Then _ seen.Add e2, 1: bcc = bcc & e2 & "; ": bccCount = bccCount + 1 added = True End If If added Then sentRows.Add r: batchN = batchN - 1 Else badRows.Add r End If End If Next r If sentRows.Count = 0 Then MsgBox "Нет видимых строк с валидным адресом.", vbInformation FlagBad ws, badRows: Exit Sub End If ' контрольные адреса Dim ctrl As Variant, ca As String For Each ctrl In Split(CONTROL_BCC, ";") ca = Trim$(CStr(ctrl)) If Len(ca) > 0 Then If Not seen.Exists(ca) Then _ seen.Add ca, 1: bcc = bcc & ca & "; ": bccCount = bccCount + 1 End If Next ctrl If Len(bcc) > 0 Then bcc = Left$(bcc, Len(bcc) - 2) Dim olApp As Object, mail As Object On Error Resume Next Set olApp = GetObject(, "Outlook.Application") If olApp Is Nothing Then Set olApp = CreateObject("Outlook.Application") On Error GoTo 0 If olApp Is Nothing Then MsgBox "Не удалось запустить Outlook.", vbCritical: Exit Sub Set mail = olApp.CreateItem(0) PrepareMail mail, htmlPath, subj If Len(TO_ADDRESS) > 0 Then mail.To = TO_ADDRESS mail.BCC = bcc mail.Display False ' показать окно, НЕ отправлять Dim msg As String msg = "Шаблон: " & tplName & vbCrLf & _ "В скрытой копии – " & bccCount & " адр. (строк: " & sentRows.Count & ")." & _ vbCrLf & vbCrLf & "Письмо отправлено?" & vbCrLf & _ "ДА – отмечу адреса как разосланные." If MsgBox(msg, vbQuestion + vbYesNo, "Подтверждение") = vbYes Then Dim stamp As String: stamp = Format$(Now, "dd.mm.yyyy hh:nn") Dim i As Long For i = 1 To sentRows.Count ws.Cells(sentRows(i), COL_STATUS).Value = "Отправлено" ws.Cells(sentRows(i), COL_DATE).Value = stamp Next i FlagBad ws, badRows MsgBox "Готово. Помечено строк: " & sentRows.Count, vbInformation Else MsgBox "Отметки не поставил – адреса попадут в следующую партию.", vbInformation End IfEnd SubPrivate Sub PrepareMail(mail As Object, htmlPath As String, subj As String) mail.Subject = subj Dim html As String Dim st As Object: Set st = CreateObject("ADODB.Stream") st.Charset = "utf-8": st.Open: st.LoadFromFile htmlPath html = st.ReadText: st.Close mail.HTMLBody = html On Error Resume Next Dim att As Object Set att = mail.Attachments.Add(LOGO_PATH, 1, 0) att.PropertyAccessor.SetProperty _ "http://schemas.microsoft.com/mapi/proptag/0x3712001F", "logo" On Error GoTo 0End SubPrivate Sub EnsureHeaders(ws As Worksheet) If Len(Trim$(CStr(ws.Cells(1, COL_STATUS).Value))) = 0 Then _ ws.Cells(1, COL_STATUS).Value = "Статус рассылки" If Len(Trim$(CStr(ws.Cells(1, COL_DATE).Value))) = 0 Then _ ws.Cells(1, COL_DATE).Value = "Дата отправки"End SubPrivate Sub FlagBad(ws As Worksheet, badRows As Collection) Dim i As Long For i = 1 To badRows.Count If Len(Trim$(CStr(ws.Cells(badRows(i), COL_STATUS).Value))) = 0 Then _ ws.Cells(badRows(i), COL_STATUS).Value = "Нет валидного email" Next iEnd Sub
Макрос привязан к кнопке на листе. Менеджер нажимает её, выбирает номер шаблона, вводит число строк – и дальше работает уже с готовым письмом в Outlook.
Что здесь стоит объяснить
mail.Display False вместо .Send – та самая точка, ради которой всё затевалось. Письмо открывается на экране, отправку делает человек.
DEFAULT_BATCH = 50 – это только значение по умолчанию в диалоге, число всё равно спрашивается при каждом запуске. После истории с лимитом менеджер вводит 25.
Проверка адресов регуляркой. В базе, собранной из выгрузок, нашлись адреса вида без точки в домене или с обрезанным доменом. Такие строки помечаются как «Нет валидного email» и в копию не попадают, иначе Exchange может отбить всё письмо целиком из-за одного битого адреса.
Scripting.Dictionary с CompareMode = 1 – дедупликация без учёта регистра. Один и тот же адрес в двух колонках или у двух заказчиков в BCC не задвоится.
Not ws.Rows(r).Hidden – эту проверку я дописал не сразу. Почему она есть расписано в ошибках.
Контрольные адреса тоже идут в скрытую копию. В таблице они не помечаются, потому что живут в константе, а не в базе, и статистику по базе не портят. Но в лимит получателей они входят наравне со всеми.
Часть 3. Письмо: HTML под Outlook
Тело письма – обычный HTML-файл на диске, макрос читает его через ADODB.Stream с явным Charset = «utf-8». Без этого кириллица превращается в кракозябры.
Сами шаблоны я не верстал руками, HTML собрал ИИ, я только гонял результат через Outlook и правил то, что разваливалось. Разваливалось многое.
Три момента стоит знать заранее.
Логотип через cid, а не ссылкой. Картинка прикрепляется вложением, а в HTML указывается src=»cid:logo» – за это отвечают две строки в PrepareMail. Так Outlook не блокирует изображение как «внешнее содержимое» и не показывает серую заглушку с предложением скачать картинки. Заодно выяснилось, что логотип в .webp Outlook не понимает вовсе – пришлось конвертировать в PNG.
Прехедер. Скрытый блок в начале письма формирует текст превью в списке входящих:
<div style="display:none;max-height:0;overflow:hidden;mso-hide:all; font-size:1px;line-height:1px;color:#f2f4f0;"> Цепляющий прехедер</div>
Кнопка. Здесь я потерял больше всего времени, поэтому вынес отдельно в блок проблемы.
Шаблонов в итоге вышло три, но в примере два так как разный продукт и разный повод обращения. Выбор шаблона – один пункт в диалоге при запуске. Какой из них работает лучше, я пока не знаю, чтобы сравнивать шаблоны, нужен объём больше моего и время.
Часть 4. Автоимпорт новых контактов из выгрузок
База пополняется из выгрузок – это отдельный Excel-файл, где почты лежат не в аккуратной колонке, а внутри многострочного текстового поля «Контакты заказчика» вперемешку с ФИО и телефонами.
Руками это разбирать бессмысленно. Отдельный макрос открывает выгрузку, вытаскивает адреса регуляркой, берёт первые два уникальных и дописывает строку в базу.
Ключевая часть – извлечение и дедупликация:
Public Sub Импорт_из_выгрузки() ' ... открытие файла через Application.GetOpenFilename ... Dim re As Object: Set re = CreateObject("VBScript.RegExp") re.Global = True: re.IgnoreCase = True re.Pattern = "[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}" ' карта заголовков выгрузки: имя колонки -> номер столбца Dim hdr As Object: Set hdr = CreateObject("Scripting.Dictionary") hdr.CompareMode = 1 Dim lastCol As Long, c As Long, hname As String lastCol = wsSrc.Cells(1, wsSrc.Columns.Count).End(xlToLeft).Column For c = 1 To lastCol hname = Trim$(CStr(wsSrc.Cells(1, c).Value)) If Len(hname) > 0 Then If Not hdr.Exists(hname) Then hdr.Add hname, c Next c ' ... для каждой строки выгрузки: Dim blob As String blob = CStr(wsSrc.Cells(r, cCont).Value) & vbLf & CStr(wsSrc.Cells(r, cOrg).Value) Dim e1 As String, e2 As String Dim uniq As Object: Set uniq = CreateObject("Scripting.Dictionary") uniq.CompareMode = 1 Dim mm As Object, k As Long, em As String Set mm = re.Execute(blob) For k = 0 To mm.Count - 1 em = mm.Item(k).Value If Not uniq.Exists(LCase$(em)) Then uniq.Add LCase$(em), 1 If e1 = "" Then e1 = em ElseIf e2 = "" Then e2 = em End If Next k ' дедупликация против существующей базы – по ИНН и по почте If e1 = "" Then noMail = noMail + 1 ElseIf (Len(innV) > 0 And innSet.Exists(innV)) Or mailSet.Exists(LCase$(e1)) Then dupCnt = dupCnt + 1 Else wsBase.Cells(destRow, T_EMAIL1).Value = e1 If e2 <> "" Then wsBase.Cells(destRow, T_EMAIL2).Value = e2 wsBase.Cells(destRow, T_INN).Value = "'" & innV ' как текст! ' ... остальные поля ... destRow = destRow + 1 End If
Три решения, которые себя оправдали.
Колонки ищутся по названию заголовка, а не по позиции. Формат выгрузки может поменяться, столбцы сдвинуться – макрос это переживёт, пока названия заголовков прежние.
Дедупликация по ИНН и по почте одновременно. На тестовой выгрузке из 18 записей нашлись два повтора одного заказчика с разным набором контактов. По ИНН они схлопнулись.
ИНН пишется с апострофом, то есть как текст. Иначе Excel съест ведущие нули и превратит его в число в экспоненциальной записи.
Новые строки попадают в базу с пустыми колонками J и K – то есть автоматически встают в очередь на ближайшую рассылку.
Какие есть риски использования скрытой копии:
Массовый BCC сам по себе выглядит подозрительно. Письмо с пустым полем «Кому» и полусотней адресов в скрытой копии. Публичных порогов никто не публикует, измеримого эффекта я вам не назову – это риск, а не число. Дешёвая страховка положить в To: собственный адрес отправителя, чтобы поле не было пустым и письмо не превращалось в undisclosed-recipients. В макросе за это отвечает константа TO_ADDRESS. Я оставил её пустой и, оглядываясь назад, зря.
Персонализации нет и не будет. Все адреса партии лежат в одном письме, поэтому «Добрый день, Иван Петрович» технически невозможно.
Если обращение по имени критично – возвращайтесь к слиянию Word или к схеме «одно письмо на адрес» и принимайте их ограничения.
Проблемы, с которыми я столкнулся
1. Attribute VB_Name – синтаксическая ошибка в первой строке
Если экспортировать модуль в .bas, его первая строка выглядит так:
Attribute VB_Name = "modRassylka"
При импорте через File → Import File редактор её обрабатывает сам. Но если открыть файл в блокноте и скопировать текст вручную в окно кода – VBA будет ругаться синтаксической ошибкой. При ручной вставке код должен начинаться с Option Explicit.
2. Variable not defined при вставке в чужой модуль
Классика Option Explicit. У меня случилось из-за того, что код импорта попал в модуль рассылки, где константа листа называется иначе: SHEET_NAME против BASE_SHEET. Лечится не переименованием, а тем, что каждый функциональный блок живёт в своём модуле со своими константами. Разные модули не конфликтуют, даже если в обоих объявлен один и тот же лист.
Полезная привычка – после вставки кода сразу жать Debug → Compile VBAProject. Ошибки вылезут до запуска, а не в момент, когда менеджер нажал кнопку.
3. Ссылки не кликаются в черновике
В открытом на редактирование письме Outlook ссылки по обычному клику не работают, только Ctrl+клик. Это защита от случайных переходов, а не сломанная вёрстка. Проверять письмо нужно на полученном экземпляре, а не на черновике.
4. Кнопка отрисовалась, но не кликается
Самая неочевидная. Outlook рендерит письма движком Word, а он не понимает border-radius и криво обрабатывает padding у ссылки. Получилось решить так:
Было – не сработало
<!--[if mso]><v:roundrect href="https://example.ru/" arcsize="16%" style="height:52px;width:290px;" fillcolor="#ABC434"> <w:anchorlock/> <center>Заказать обратный звонок</center></v:roundrect><![endif]-->
Кнопка стала красивой и скруглённой. И перестала быть ссылкой. В моей сборке Outlook она отрисовалась как фигура, клик по ней не делал ничего. При этом обычные текстовые ссылки в подвале работали.
Итоговое решение – отказаться от VML в пользу обычной ссылки в ячейке таблицы:
<table role="presentation" cellspacing="0" cellpadding="0" border="0"><tr> <td align="center" bgcolor="#ABC434" style="border-radius:8px;"> <a href="https://example.ru/" target="_blank" style="display:inline-block;padding:16px 36px;font-family:Arial,sans-serif; font-size:16px;font-weight:bold;color:#1a1a1a;text-decoration:none; border-radius:8px;">Заказать обратный звонок</a> </td></tr></table>
В Outlook углы будут почти прямые, зато кликается весь зелёный блок. В Gmail и на мобильных скругление отработает нормально. Размен «работает» против «идеально круглое» я решил в пользу первого – и дополнительно положил под кнопкой текстовую ссылку в духе «Кнопка не открывается? Перейдите по ссылке».
5. Макрос не видит фильтр
Замысел был такой: менеджер ставит автофильтр по типу продукта, оставляет на экране нужный кластер и запускает рассылку по нему.
Цикл For r = … To lastRow про фильтр ничего не знает. Скрытая фильтром строка для него – обычная строка, и партия набралась бы с начала таблицы, независимо от того, что на экране. Причём отметки «Отправлено» проставились бы честно, так что по таблице это не увидишь.
Лечится одной проверкой:
If Not ws.Rows(r).Hidden And Len(Trim$(CStr(ws.Cells(r, COL_STATUS).Value))) = 0 Then
Rows(r).Hidden возвращает True и для скрытых фильтром строк, и для скрытых вручную. Теперь партия набирается только из видимой выборки.
Если разослать часть кластера и сменить фильтр, то отправленные будут разбросаны по таблице до тех пор, пока не вернёшь тот же фильтр. Это не баг, а прямое следствие работы по видимой области.
Ограничения решения и что с ними делать
Отметка «Отправлено» опирается на слова человека. Менеджер может нажать «Да», не отправив письмо, и в базе появится ложная отметка. Технически надёжнее ловить реальную отправку через событие Application.ItemSend, но это требует кода уже в самом Outlook, что в корпоративной среде опять упирается в согласования.
Можно источником истины делать не ответ человека, а контрольную копию. Контрольные адреса и так уходят в BCC каждой партии – заведите правило, складывающее их в отдельную папку. Пришло письмо – партия действительно ушла, и время его получения сверяется с датой в столбце K. Расхождение видно сразу.
Битые адреса. У меня их оказалось около 10% базы (домены с опечатками, обрезанные хвосты, адреса, которые за время между сбором и рассылкой перестали существовать). Регулярка ловит только синтаксически невалидные, остальные возвращаются NDR-письмами, которые никто не собирает автоматически. Минимальная гигиена – правило Outlook, отлавливающее возвраты от postmaster в отдельную папку, и раз в неделю – проставить в столбце J статус «Не доставлено». Без этого база деградирует, а вы каждый раз тратите лимит на мёртвые адреса.
Нет статистики внутри письма. Ни открытий, ни отписок. Что письмо прочитали, вы не узнаете.
Частично я это решил UTM-метками. Все ссылки в шаблонах я разметил, так что переходы на сайт видно в аналитике. Это единственный количественный сигнал, который у меня есть.
Лимиты – на вас. Мои 50 – настройка нашего администратора. Ваши будут другими.
Что получилось в итоге
Около 500 адресов за неделю с небольшим, по две партии в день. Ни одной установки софта, ни одного согласования сверх того, что и так требовалось. Менеджер нажимает кнопку на листе, выбирает шаблон и число строк, потом нажимает «Отправить» в открывшемся письме. Всё остальное – сборка адресов из видимого сегмента, дедупликация, отсев битых, вёрстка, логотип, контрольные копии, отметки с датой и пополнение базы из выгрузок – делает макрос.
Прямых откликов не было, были переходы на сайт (в аналитике видно, что письма читают и по ним ходят). Для холодной рассылки это ожидаемый результат.
Это не замена сервису рассылок и не претендует на неё. Это способ не останавливать работу, пока сервис согласовывают.
Если будете повторять, начните с трёх вещей в таком порядке. Спросите у IT свой лимит получателей. Посмотрите настройку программного доступа в Центре управления безопасностью Outlook, возможно, вам хватит простого .Send. И сделайте прогон на одного получателя, вписав в базу собственный адрес.
ссылка на оригинал статьи https://habr.com/ru/articles/1065760/