Позднее Ctrl + ↑

Прощай Яндекс почта

Была у меня почта на Яндексе, уже была, т. к. последний сделал её платной. Что же, я перевёз свои 12 ящиков, и вы справитесь.

Схема самая простая, сначала разворачиваем свой почтовый сервер (настраиваем его), затем перевозим ящики (по одному), бонусом тюним web-интерфейс Roundcube.

Все представленные здесь решения являются бесплатными для личного пользования, ваша задача только оплата VDS.

Системные требования для моих задач (12 ящиков, 1 основной домен и 3 элиаса):

  • OC Linux
  • CPU 1
  • RAM 4 Гб
  • HDD 100 Гб
  • IP 1

Возможно вам потребуется больше, тут всё зависит от бюджета и нагрузки, системные требования для iRedMail предполагают от 4Гб оперативной памяти, а для поддержки 500 клиентов так все 16Гб.

Я не буду останавливаться на развёртывание системы, в моём примере debian 11, многие хостеры это сделаю для вас автоматически.
Не забудьте прописать в DNS зону A, например mail.{domain}.ru

Прошу обратить внимание, зону MX мы не трогаем, чтобы пока мы настраиваем новый сервер, не потерять сообщения приходящие на старый.

коннектимся в консоль сервера и создаем пользователя с sudo

su -
apt update
apt install mc sudo -y
useradd {username}
passwd {username}
usermod -aG sudo {username}
  • mc ставлю ради mcedit

Логинимся под свеже созданным пользователем и назначаем серверу его имя как указывали в DNS

sudo hostnamectl set-hostname mail.{domain}.ru

в файле «/etc/hosts» руками правим имя локалхоста

sudo mcedit /etc/hosts

должно получиться так

127.0.0.1       mail.{domain}.ru localhost

проверяем

hostname -f

Начинаем непосредственно установку сервера, для этого определяем ссылку на актуальный дистрибутив, переходим сюда: https://iredmail.com/download.html , кликаем правой кнопкой мыши на стабильной версии и копируем ссылку на скачивание

в консоли идем в домашнюю папку и скачиваем дистрибутив

cd ~
wget https://github.com/iredmail/iRedMail/archive/refs/tags/1.6.2.tar.gz

распаковываем его, переходим в полученную папку делаем файл «iRedMail-1.6.2» исполняемым и запускаем установку

tar xvf 1.6.2.tar.gz
cd iRedMail-1.6.2/
chmod +x iRedMail.sh
sudo bash iRedMail.sh

после того как скрипт скачет необходимые пакеты запустится визард, который поможет провести первоначальную настройку

указываем путь, где будут храниться почтовые сообщения и бэкапы базы данных

выбираем какой web-сервер будет установлен, ну как выбираем, соглашаемся с nginx

далее выбираем какую базу данных будет использовать почтовый сервер (я выбираю MariaDB)

придумываем и указываем пароль для администратора базы данных, можно сейчас записать, а можно дождаться окончания установки и получить все логины и пароли в первом письме

указываем имя домена, не имя сервера, а имя домена!

задаём пароль администратора почтового сервера

указываем необходимые нам компоненты, для простого и лёгкого сервера я бы рекомендовал остановиться на почтовом web-клиенте Roundcube, web-панели администратора iRedAdmin и fail2ban для защиты от подбора паролей пользователей

проверяем настройки и подтверждаем их

теперь остаётся ждать, пока инсталляционный скрипт завершит свою работу

после завершения установки подтвердите настройки файервола

Я вас поздравляю, теперь можно получить доступ к ресурсам сервера по следующим адресам:

- Roundcube webmail: https://mail.{domain}.ru/mail/
- Web admin panel (iRedAdmin): https://mail.{domain}.ru/iredadmin/

- Username: postmaster@{domain}.ru
- Password: BTAyZswFv4VroUP2oPTUkaikMETc6pyY

Если захотите добавить элиас домена через бесплатную админку этого сделать не получится, но можно внести значения напрямую в базу (работает как с MySQL, так и с PostgreSQL)

mysql -u root
sql> USE `vmail`;
sql> INSERT INTO `alias_domain` (`alias_domain`, `target_domain`) VALUES ('{alias}.ru', '{domain}.ru');

удалить элиас так:

mysql -u root
sql> USE `vmail`;
sql> DELETE FROM `alias_domain` WHERE `alias_domain` = '{alias}.ru';

На этом можно было бы и закончить, но есть моменты на которых я предлагаю остановиться, назовём это улучшения вашего сервера


Система индексации lucene для Dovecot (debian)


Отключаем протокол pop3 (делал чисто для себя, т.к .не пользуюсь им, слишком устаревший)


Выпускаем подтверждённый сертификат Let’s encrypt и настраиваем автоматический перевыпуск сертификата


Как увеличить размер принимаемого сообщения, например до 35Мб?


Есть проблемы с антивирусом clamav на территории РФ, после первоначальной установки он у вас просто не запустится, т. к. не сможет обновить свои базы, как решить?


готово! наш сервер готов принять письма

Через Админку создаём пользовательские ящики, которые готовы принять письма

Меняем запись MX в DNS, чтобы новые сообщения попадали уже на наш сервер

Ждём когда изменения вступят в силу, максимум 3 часа, вы увидите, как сообщения появятся в ящиках на новом сервере, всё — это значит пора перевозить старые письма. Для этого воспользуемся моим скриптом: https://voronin.one/all/skript-dlya-perenosa-elektronnoy-pochty-iz-odnogo-yaschika-v-dru/

Адресную книгу без проблем перенесёте через экспорт/импорт.
А вот правила фильтрации в яндексе «фильдеперсовые», их только вручную можно перенести, хоть в Roundcube есть возможность подгрузить правила sieve из файла.

скрипт для переноса электронной почты из одного ящика в другой

Скрипт максимально простой, написан на php, так же потребуется модуль php-imap, работает из командной строки любого устройства (сервера). Принцип простой, скрипт подключается к исходному ящику по протоколу IMAP сканирует структуру почтовых папок, подключается к целевому ящику IMAP, воссоздаёт папки которых нет и загружает в них письма. Опционально может создать структуру папок локально и скачать в них письма в формате eml.

<?php

//папка, куда будет сохраняться структура ящика с письмами
$path_to_mail = 'boxes';

//сохранять локально копию ящика
$save_local_copy = 0;

//декодировать имя папки или нет для локальной копии
$decode_folder_name = 1;

//данные ящика откуда переезжаем (например яндекс)
$source_server = 'imap.yandex.ru';
$source_port = '993';
$source_user = '{user}@{domain}.ru';
$source_pass = 'P@$$word';

//данные ящика куда переезжаем
$target_server = 'mail.{domain}.ru';
$target_port = '993';
$target_user = '{user}@{domain}.ru';
$target_pass = 'P@$$word';

function check_folder_exist($folders_list, $check_folder_name) {
  $fl_exist = false;
  foreach ($folders_list as $val) {
    if ($val->name == $check_folder_name) {
      $fl_exist = true;
    }
  }
  return $fl_exist;
}

$path_to_mail .= DIRECTORY_SEPARATOR.$source_user.DIRECTORY_SEPARATOR;

$source_server_str = "{".$source_server.":".$source_port."/imap/ssl}";
$source_mbox = imap_open($source_server_str,$source_user,$source_pass)
      or die("can't connect: " . imap_last_error());

$target_server_str = "{".$target_server.":".$target_port."/imap/ssl}";
$target_mbox = imap_open($target_server_str,$target_user,$target_pass)
      or die("can't connect: " . imap_last_error());

$target_list = imap_getmailboxes($target_mbox, $target_server_str, "*");
if (is_array($target_list)) {
    foreach ($target_list as $key => $val) {
      $folder_name = str_replace($target_server_str,'',$val->name);
      $target_delimiter = $val->delimiter;
    }
} else {
    echo "imap_getmailboxes failed: ".imap_last_error()."\n";
}

$source_list = imap_getmailboxes($source_mbox, $source_server_str, "*");
if (is_array($source_list)) {
    foreach ($source_list as $key => $val) {
        $source_full_folder_name = $val->name;
        $source_delimiter = $val->delimiter;
        $con=imap_open($source_full_folder_name, $source_user, $source_pass);
        $number_msg=imap_num_msg($con);
        $folder_name = str_replace($source_server_str,'',$source_full_folder_name);
        if ($decode_folder_name) {
          $current_folder_name = mb_convert_encoding($folder_name, "UTF8", "UTF7-IMAP");
        } else {
          $current_folder_name = $folder_name;
        }
        $full_new_folder_name = $target_server_str.str_replace($source_delimiter,$target_delimiter,$folder_name);
        echo $full_new_folder_name."\n";
        if (!(check_folder_exist($target_list, $full_new_folder_name))) {
          echo 'папка "'.mb_convert_encoding($folder_name, "UTF8", "UTF7-IMAP").'" отсутствует на сервере, создаём'."\n";
          
          imap_createmailbox($target_mbox, $full_new_folder_name);
        }
        if ((!file_exists($path_to_mail.str_replace($source_delimiter,DIRECTORY_SEPARATOR,$current_folder_name))) and ($save_local_copy)) {
            mkdir($path_to_mail.str_replace($source_delimiter,DIRECTORY_SEPARATOR,$current_folder_name), 0777, true);
        }
        echo mb_convert_encoding($folder_name, "UTF8", "UTF7-IMAP").": ";
        echo $number_msg."\n";
        for ($i = 1; $i <= $number_msg; $i++) {
          if (!imap_ping($con)) {
              // если вдруг связь первалась, восстанавливаем
              echo "$i ой, reconnect $source_server\n";
              $i--;
              $con=imap_open($val->name, $source_user, $source_pass);
          }
          if (!imap_ping($target_mbox)) {
              // если вдруг связь первалась, восстанавливаем
              echo "$i ой, reconnect $target_server\n";
              $target_mbox = imap_open($target_server_str,$target_user,$target_pass);
          }
          echo ".";
          $headers = imap_fetchheader($con, $i, FT_PREFETCHTEXT);
          $body = imap_body($con, $i);
          imap_append($target_mbox, $full_new_folder_name, $headers."\n\n".$body,"\\Seen");
          if ($save_local_copy) {
            file_put_contents($path_to_mail.str_replace($val->delimiter,DIRECTORY_SEPARATOR,$current_folder_name).DIRECTORY_SEPARATOR.str_pad($i, 8, "0", STR_PAD_LEFT).".eml", $headers."\n\n".$body);
          }
        }
        echo "\n";
      imap_close($con);
    }
} else {
    echo "imap_getmailboxes failed: ".imap_last_error()."\n";
}
imap_close($source_mbox);
imap_close($target_mbox);

?>

$path_to_mail — относительный путь к папке, где будет храниться локальная копия ящика, актуально когда «$save_local_copy = 1»

$save_local_copy = 1 будет сохраняться локальная копия писем на жёстком диске, соответственно, если «0» — не будет

$decode_folder_name = 1 имена папок будут декодированы в нормальный вид, если «0» — то будет использован формат UTF7, как они хранятся на сервере.

Все письма, которые будут перенесены в новый ящик получат статус прочитанных, извините, я не заморачивался с перенесением флагов.

Перед прочтением сообщения пингуется соединение с сервером IMAP, в случае его обрыва — соединение восстанавливается. Замедление совсем небольшое, зато надёжность вырастает многократно.

Учитываются разделители вложенности папок (например у яндекса это «|», в iRedMail «/»).

Копирование писем допускается один раз, если вы прервёте процесс и запустите снова, часть сообщений задублируется.

автоматизация фитолампы

Растут у меня красивые растения Адениум обесум, но пока маленькие и некрасивые. 😉 Холодной и хмурой зимой хочется даль больше света и тепла малявкам, а для этого на выручку — придёт фитолампа, у меня длинная над подоконником висит и питается 12В.

Конечно же автоматизировать процесс включения и отключения. Для этого на Алике мною было приобретено реле с датчиком света:

с помощью проводов с разъёмами на 12В я собрал простенькую схему для подключения в разрыв цепи

примерно так:

И в процессе тестов мои подозрения подтвердились, в момент переключения реле начинает щёлкать, безостановочно то включаясь, то отключаясь. Почему так происходило? Темнеет очень плавно, когда наступает пороговое значение — датчик освещенности дает сигнал на включение фитолампы, общая освещенность помещения немного увеличивается, но этого достаточно, чтобы сработал датчик и выключил лампу и так по кругу. Такой процесс происходит длительный период, переключая туда-сюда по несколько раз в секунду.

Правильно, для света и тепла использовать датчики с двумя порогами и разносить точки включения и отключения, создавая петлю Гистерезиса.

С текущим устройством реализовать подобную схему не получится, пороговая точка одна. К сожалению, разбираем и думаем другое решение.

Новый проект

А с решением мне помог мой коллега Дмитрий, он подсказал, что проще всего привязаться ко времени восхода и захода солнца.

Приступаем:

  • самое главное — синхронизация времени
  • график включения/отключения для нашей полосы

Остановил свой выбор на ESP8266 с WiFi модулем Nodemcu-CP2102.

Алгоритм следующий: подключились к wifi, синхронизировали время с ntp сервером, и с определенной периодичностью проверяем вхождение времени в массивы вкл/откл.

Я проверил время восходов и закатов из года в год примерно одинаковое, ± 1 минута, что не существенная погрешность. Используем готовые массивы, которые можно получить из онлайн калькуляторов для своего местоположения (в примере для Москвы).

Схема подключения очень простая, в качестве ключа я использовал мосфет 30N06, благо, в своё время куплено их было много

Он спокойно работает с напряжением 12В и током 1А, и переключается рабочим напряжением 3,3В с ноги ардуино.

Схема подключения взята из ролика Алекса Гайвера

Питание 12В я подал на разъём VIN и GND, а управление повесил на ногу D1.

Ниже сам код, в комментариях я постарался максимально подробно расписать механизмы

#include <NTPClient.h>
#include <ESP8266WiFi.h>
#include <WiFiUdp.h>

const char *ssid     = "wifi_name";
const char *password = "wifi_password";
char buf[6];

// массивы с временем в формате день:месяц-часы:минуты, дополненные нулями
String time_lamp_off[366] = { "01.01-09:00", "02.01-09:00", "03.01-08:59", "04.01-08:59", "05.01-08:59", "06.01-08:58", "07.01-08:57", "08.01-08:57", "09.01-08:56", "10.01-08:55", "11.01-08:54", "12.01-08:54", "13.01-08:53", "14.01-08:52", "15.01-08:51", "16.01-08:49", "17.01-08:48", "18.01-08:47", "19.01-08:46", "20.01-08:44", "21.01-08:43", "22.01-08:42", "23.01-08:40", "24.01-08:39", "25.01-08:37", "26.01-08:35", "27.01-08:34", "28.01-08:32", "29.01-08:30", "30.01-08:29", "31.01-08:27", "01.02-08:25", "02.02-08:23", "03.02-08:21", "04.02-08:19", "05.02-08:17", "06.02-08:15", "07.02-08:13", "08.02-08:11", "09.02-08:09", "10.02-08:07", "11.02-08:05", "12.02-08:03", "13.02-08:00", "14.02-07:58", "15.02-07:56", "16.02-07:54", "17.02-07:51", "18.02-07:49", "19.02-07:47", "20.02-07:44", "21.02-07:42", "22.02-07:40", "23.02-07:37", "24.02-07:35", "25.02-07:32", "26.02-07:30", "27.02-07:27", "28.02-07:25", "29.02-07:25", "01.03-07:23", "02.03-07:20", "03.03-07:18", "04.03-07:15", "05.03-07:12", "06.03-07:10", "07.03-07:07", "08.03-07:05", "09.03-07:02", "10.03-07:00", "11.03-06:57", "12.03-06:54", "13.03-06:52", "14.03-06:49", "15.03-06:47", "16.03-06:44", "17.03-06:41", "18.03-06:39", "19.03-06:36", "20.03-06:33", "21.03-06:31", "22.03-06:28", "23.03-06:26", "24.03-06:23", "25.03-06:20", "26.03-06:18", "27.03-06:15", "28.03-06:12", "29.03-06:10", "30.03-06:07", "31.03-06:04", "01.04-06:02", "02.04-05:59", "03.04-05:57", "04.04-05:54", "05.04-05:51", "06.04-05:49", "07.04-05:46", "08.04-05:44", "09.04-05:41", "10.04-05:39", "11.04-05:36", "12.04-05:33", "13.04-05:31", "14.04-05:28", "15.04-05:26", "16.04-05:23", "17.04-05:21", "18.04-05:18", "19.04-05:16", "20.04-05:14", "21.04-05:11", "22.04-05:09", "23.04-05:06", "24.04-05:04", "25.04-05:02", "26.04-04:59", "27.04-04:57", "28.04-04:55", "29.04-04:52", "30.04-04:50", "01.05-04:48", "02.05-04:46", "03.05-04:43", "04.05-04:41", "05.05-04:39", "06.05-04:37", "07.05-04:35", "08.05-04:33", "09.05-04:31", "10.05-04:29", "11.05-04:27", "12.05-04:25", "13.05-04:23", "14.05-04:21", "15.05-04:19", "16.05-04:18", "17.05-04:16", "18.05-04:14", "19.05-04:12", "20.05-04:11", "21.05-04:09", "22.05-04:08", "23.05-04:06", "24.05-04:05", "25.05-04:03", "26.05-04:02", "27.05-04:00", "28.05-03:59", "29.05-03:58", "30.05-03:57", "31.05-03:55", "01.06-03:54", "02.06-03:53", "03.06-03:52", "04.06-03:51", "05.06-03:51", "06.06-03:50", "07.06-03:49", "08.06-03:48", "09.06-03:48", "10.06-03:47", "11.06-03:47", "12.06-03:46", "13.06-03:46", "14.06-03:46", "15.06-03:45", "16.06-03:45", "17.06-03:45", "18.06-03:45", "19.06-03:45", "20.06-03:45", "21.06-03:45", "22.06-03:45", "23.06-03:46", "24.06-03:46", "25.06-03:46", "26.06-03:47", "27.06-03:47", "28.06-03:48", "29.06-03:49", "30.06-03:49", "01.07-03:50", "02.07-03:51", "03.07-03:52", "04.07-03:53", "05.07-03:54", "06.07-03:55", "07.07-03:56", "08.07-03:57", "09.07-03:58", "10.07-03:59", "11.07-04:01", "12.07-04:02", "13.07-04:03", "14.07-04:05", "15.07-04:06", "16.07-04:08", "17.07-04:09", "18.07-04:11", "19.07-04:12", "20.07-04:14", "21.07-04:15", "22.07-04:17", "23.07-04:19", "24.07-04:21", "25.07-04:22", "26.07-04:24", "27.07-04:26", "28.07-04:28", "29.07-04:29", "30.07-04:31", "31.07-04:33", "01.08-04:35", "02.08-04:37", "03.08-04:39", "04.08-04:40", "05.08-04:42", "06.08-04:44", "07.08-04:46", "08.08-04:48", "09.08-04:50", "10.08-04:52", "11.08-04:54", "12.08-04:56", "13.08-04:58", "14.08-05:00", "15.08-05:02", "16.08-05:04", "17.08-05:06", "18.08-05:07", "19.08-05:09", "20.08-05:11", "21.08-05:13", "22.08-05:15", "23.08-05:17", "24.08-05:19", "25.08-05:21", "26.08-05:23", "27.08-05:25", "28.08-05:27", "29.08-05:29", "30.08-05:31", "31.08-05:33", "01.09-05:35", "02.09-05:37", "03.09-05:39", "04.09-05:40", "05.09-05:42", "06.09-05:44", "07.09-05:46", "08.09-05:48", "09.09-05:50", "10.09-05:52", "11.09-05:54", "12.09-05:56", "13.09-05:58", "14.09-06:00", "15.09-06:02", "16.09-06:03", "17.09-06:05", "18.09-06:07", "19.09-06:09", "20.09-06:11", "21.09-06:13", "22.09-06:15", "23.09-06:17", "24.09-06:19", "25.09-06:21", "26.09-06:23", "27.09-06:25", "28.09-06:27", "29.09-06:28", "30.09-06:30", "01.10-06:32", "02.10-06:34", "03.10-06:36", "04.10-06:38", "05.10-06:40", "06.10-06:42", "07.10-06:44", "08.10-06:46", "09.10-06:48", "10.10-06:50", "11.10-06:52", "12.10-06:54", "13.10-06:56", "14.10-06:58", "15.10-07:00", "16.10-07:02", "17.10-07:04", "18.10-07:06", "19.10-07:08", "20.10-07:11", "21.10-07:13", "22.10-07:15", "23.10-07:17", "24.10-07:19", "25.10-07:21", "26.10-07:23", "27.10-07:25", "28.10-07:27", "29.10-07:29", "30.10-07:31", "31.10-07:34", "01.11-07:36", "02.11-07:38", "03.11-07:40", "04.11-07:42", "05.11-07:44", "06.11-07:46", "07.11-07:48", "08.11-07:51", "09.11-07:53", "10.11-07:55", "11.11-07:57", "12.11-07:59", "13.11-08:01", "14.11-08:03", "15.11-08:05", "16.11-08:07", "17.11-08:09", "18.11-08:11", "19.11-08:13", "20.11-08:15", "21.11-08:17", "22.11-08:19", "23.11-08:21", "24.11-08:23", "25.11-08:25", "26.11-08:27", "27.11-08:29", "28.11-08:30", "29.11-08:32", "30.11-08:34", "01.12-08:35", "02.12-08:37", "03.12-08:39", "04.12-08:40", "05.12-08:42", "06.12-08:43", "07.12-08:45", "08.12-08:46", "09.12-08:47", "10.12-08:48", "11.12-08:50", "12.12-08:51", "13.12-08:52", "14.12-08:53", "15.12-08:54", "16.12-08:55", "17.12-08:56", "18.12-08:56", "19.12-08:57", "20.12-08:58", "21.12-08:58", "22.12-08:59", "23.12-08:59", "24.12-09:00", "25.12-09:00", "26.12-09:00", "27.12-09:00", "28.12-09:00", "29.12-09:00", "30.12-09:00", "31.12-09:00" };
String time_lamp_on[366] = { "01.01-16:07", "02.01-16:08", "03.01-16:10", "04.01-16:11", "05.01-16:12", "06.01-16:14", "07.01-16:15", "08.01-16:17", "09.01-16:18", "10.01-16:20", "11.01-16:21", "12.01-16:23", "13.01-16:25", "14.01-16:27", "15.01-16:28", "16.01-16:30", "17.01-16:32", "18.01-16:34", "19.01-16:36", "20.01-16:38", "21.01-16:40", "22.01-16:42", "23.01-16:44", "24.01-16:46", "25.01-16:48", "26.01-16:50", "27.01-16:52", "28.01-16:54", "29.01-16:56", "30.01-16:58", "31.01-17:01", "01.02-17:03", "02.02-17:05", "03.02-17:07", "04.02-17:09", "05.02-17:11", "06.02-17:14", "07.02-17:16", "08.02-17:18", "09.02-17:20", "10.02-17:22", "11.02-17:25", "12.02-17:27", "13.02-17:29", "14.02-17:31", "15.02-17:33", "16.02-17:36", "17.02-17:38", "18.02-17:40", "19.02-17:42", "20.02-17:44", "21.02-17:46", "22.02-17:48", "23.02-17:51", "24.02-17:53", "25.02-17:55", "26.02-17:57", "27.02-17:59", "28.02-18:01", "29.02-18:02", "01.03-18:03", "02.03-18:05", "03.03-18:08", "04.03-18:10", "05.03-18:12", "06.03-18:14", "07.03-18:16", "08.03-18:18", "09.03-18:20", "10.03-18:22", "11.03-18:24", "12.03-18:26", "13.03-18:28", "14.03-18:30", "15.03-18:32", "16.03-18:34", "17.03-18:36", "18.03-18:38", "19.03-18:40", "20.03-18:42", "21.03-18:44", "22.03-18:46", "23.03-18:48", "24.03-18:50", "25.03-18:52", "26.03-18:54", "27.03-18:56", "28.03-18:58", "29.03-19:00", "30.03-19:02", "31.03-19:04", "01.04-19:06", "02.04-19:08", "03.04-19:10", "04.04-19:12", "05.04-19:14", "06.04-19:16", "07.04-19:18", "08.04-19:20", "09.04-19:22", "10.04-19:24", "11.04-19:26", "12.04-19:29", "13.04-19:31", "14.04-19:33", "15.04-19:35", "16.04-19:37", "17.04-19:39", "18.04-19:41", "19.04-19:43", "20.04-19:45", "21.04-19:47", "22.04-19:49", "23.04-19:51", "24.04-19:53", "25.04-19:55", "26.04-19:57", "27.04-19:59", "28.04-20:01", "29.04-20:03", "30.04-20:05", "01.05-20:07", "02.05-20:09", "03.05-20:11", "04.05-20:13", "05.05-20:15", "06.05-20:17", "07.05-20:19", "08.05-20:21", "09.05-20:23", "10.05-20:25", "11.05-20:27", "12.05-20:29", "13.05-20:30", "14.05-20:32", "15.05-20:34", "16.05-20:36", "17.05-20:38", "18.05-20:40", "19.05-20:42", "20.05-20:43", "21.05-20:45", "22.05-20:47", "23.05-20:48", "24.05-20:50", "25.05-20:52", "26.05-20:53", "27.05-20:55", "28.05-20:56", "29.05-20:58", "30.05-20:59", "31.05-21:01", "01.06-21:02", "02.06-21:04", "03.06-21:05", "04.06-21:06", "05.06-21:07", "06.06-21:08", "07.06-21:10", "08.06-21:11", "09.06-21:12", "10.06-21:12", "11.06-21:13", "12.06-21:14", "13.06-21:15", "14.06-21:16", "15.06-21:16", "16.06-21:17", "17.06-21:17", "18.06-21:18", "19.06-21:18", "20.06-21:19", "21.06-21:19", "22.06-21:19", "23.06-21:19", "24.06-21:19", "25.06-21:19", "26.06-21:19", "27.06-21:19", "28.06-21:19", "29.06-21:19", "30.06-21:18", "01.07-21:18", "02.07-21:17", "03.07-21:17", "04.07-21:16", "05.07-21:16", "06.07-21:15", "07.07-21:14", "08.07-21:13", "09.07-21:12", "10.07-21:11", "11.07-21:10", "12.07-21:09", "13.07-21:08", "14.07-21:07", "15.07-21:06", "16.07-21:05", "17.07-21:03", "18.07-21:02", "19.07-21:01", "20.07-20:59", "21.07-20:58", "22.07-20:56", "23.07-20:54", "24.07-20:53", "25.07-20:51", "26.07-20:49", "27.07-20:48", "28.07-20:46", "29.07-20:44", "30.07-20:42", "31.07-20:40", "01.08-20:38", "02.08-20:36", "03.08-20:34", "04.08-20:32", "05.08-20:30", "06.08-20:28", "07.08-20:26", "08.08-20:24", "09.08-20:22", "10.08-20:20", "11.08-20:17", "12.08-20:15", "13.08-20:13", "14.08-20:11", "15.08-20:08", "16.08-20:06", "17.08-20:04", "18.08-20:01", "19.08-19:59", "20.08-19:57", "21.08-19:54", "22.08-19:52", "23.08-19:49", "24.08-19:47", "25.08-19:44", "26.08-19:42", "27.08-19:39", "28.08-19:37", "29.08-19:34", "30.08-19:32", "31.08-19:29", "01.09-19:27", "02.09-19:24", "03.09-19:22", "04.09-19:19", "05.09-19:16", "06.09-19:14", "07.09-19:11", "08.09-19:09", "09.09-19:06", "10.09-19:03", "11.09-19:01", "12.09-18:58", "13.09-18:55", "14.09-18:53", "15.09-18:50", "16.09-18:47", "17.09-18:45", "18.09-18:42", "19.09-18:39", "20.09-18:37", "21.09-18:34", "22.09-18:32", "23.09-18:29", "24.09-18:26", "25.09-18:24", "26.09-18:21", "27.09-18:18", "28.09-18:16", "29.09-18:13", "30.09-18:10", "01.10-18:08", "02.10-18:05", "03.10-18:02", "04.10-18:00", "05.10-17:57", "06.10-17:55", "07.10-17:52", "08.10-17:50", "09.10-17:47", "10.10-17:44", "11.10-17:42", "12.10-17:39", "13.10-17:37", "14.10-17:34", "15.10-17:32", "16.10-17:29", "17.10-17:27", "18.10-17:24", "19.10-17:22", "20.10-17:20", "21.10-17:17", "22.10-17:15", "23.10-17:12", "24.10-17:10", "25.10-17:08", "26.10-17:05", "27.10-17:03", "28.10-17:01", "29.10-16:59", "30.10-16:56", "31.10-16:54", "01.11-16:52", "02.11-16:50", "03.11-16:48", "04.11-16:46", "05.11-16:44", "06.11-16:42", "07.11-16:40", "08.11-16:38", "09.11-16:36", "10.11-16:34", "11.11-16:32", "12.11-16:30", "13.11-16:28", "14.11-16:27", "15.11-16:25", "16.11-16:23", "17.11-16:22", "18.11-16:20", "19.11-16:18", "20.11-16:17", "21.11-16:15", "22.11-16:14", "23.11-16:13", "24.11-16:11", "25.11-16:10", "26.11-16:09", "27.11-16:08", "28.11-16:07", "29.11-16:05", "30.11-16:04", "01.12-16:04", "02.12-16:03", "03.12-16:02", "04.12-16:01", "05.12-16:00", "06.12-16:00", "07.12-15:59", "08.12-15:59", "09.12-15:58", "10.12-15:58", "11.12-15:57", "12.12-15:57", "13.12-15:57", "14.12-15:57", "15.12-15:57", "16.12-15:57", "17.12-15:57", "18.12-15:57", "19.12-15:57", "20.12-15:58", "21.12-15:58", "22.12-15:59", "23.12-15:59", "24.12-16:00", "25.12-16:00", "26.12-16:01", "27.12-16:02", "28.12-16:03", "29.12-16:04", "30.12-16:05", "31.12-16:06" };

// переопределяем +3 часа к Гринвичу
const long utcOffsetInSeconds = 10800;
// задаем частоту синхронизации 1 час
const long UpdateIntervalInMilliseconds = 3600000;

// Определение NTP-клиента для получения времени
WiFiUDP ntpUDP;
// синхронизируем время с ntp сервером pool.ntp.org
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds, UpdateIntervalInMilliseconds);

// функция дополнения числа нулём
String c2d (byte i) { 
// добавляю ноль перед числом до 10 и преобразовываю в строку
  String str = (String)i;
  if (i < 10)  str = '0' + str; 
  return str;
}

void setup() {
  Serial.begin(115200);
  // Подготовка GPIO
  pinMode(5, OUTPUT);
  digitalWrite(5, 0);

  WiFi.begin(ssid, password);
  while ( WiFi.status() != WL_CONNECTED ) {
    delay ( 500 );
    Serial.print ( "." );
  }
  Serial.println("");
  Serial.println("WiFi connected");
// в случае обрыва соединения Wifi - автоматически переподключаемся
  WiFi.setAutoReconnect(true);
  WiFi.persistent(true);
  timeClient.begin();
}

void loop() 
{
  timeClient.update();

// считываем часы и минуты
  int currentHour = timeClient.getHours();
  int currentMinute = timeClient.getMinutes();
  time_t epochTime = timeClient.getEpochTime();
  struct tm *ptm = gmtime ((time_t *)&epochTime);
// считываем число и месяц
  int monthDay = ptm->tm_mday;
  int currentMonth = ptm->tm_mon;
// формируем строку в соответствие с форматом в массивах для последующего сравнения
  String currentDateTime = c2d(monthDay)+"."+c2d(currentMonth+1)+"-"+c2d(currentHour)+":"+c2d(currentMinute);
  Serial.println(currentDateTime);

// проходим в цикле значения массива отключения и в случае совпадения - отключаем лампу
  for (int i = 0; i < 366; i++) {
    if (time_lamp_off[i] == currentDateTime) {
      Serial.println("Off lamp");
      digitalWrite(5, 0);
      break;
    }
 }
// проходим в цикле значения массива включения и в случае совпадения - включаем лампу
  for (int i = 0; i < 366; i++) {
    if (time_lamp_on[i] == currentDateTime) {
      Serial.println("On lamp");
      digitalWrite(5, 1);
      break;
    }
 }

// пауза - 20 сек. проверку осуществляем 3 раза в минуту, в случае, если что-то один раз сбоит - перепроверим еще пару раз, просто добавляем надёжности
  delay(20000);
}

Собрано было в компактный корпус, тоже с Алика

Теперь всё работает как часы, в прямом и переносном смысле 😅

Ранее Ctrl + ↓