Почтовая рассылка сервисами Google

Andrei Novichkov | 27 июня, 2019

Введение

Задача организации почтовой рассылки вполне может возникнуть у трейдера, поддерживающего деловые отношения с другими трейдерами, с подписчиками, клиентами, даже просто с друзьями. Разослать скриншоты, какие-то журналы, логи, или отчеты — это вполне актуальные задачи, востребованные не каждый день, но и не так уж редко, в любом случае хотелось бы обладать такой возможностью. Использование штатных средств MQL здесь определенно затруднительно, а то и просто невозможно. В конце статьи мы вернемся к вопросу об использовании только MQL для решения данной задачи, пока же мы будем использовать кроме MQL еще и C#. Это даст нам возможность сравнительно легко написать необходимый код и подключить его к терминалу, но и поставит перед нами весьма интересную задачу, связанную с этим подключением.

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

Постановка задачи

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

Далее сразу возникает вопрос об управлении этим списком. Какие тут могут быть варианты?

  1. База данных на жестком диске, или CSV файл неудобны и не достаточно надежны. Они далеко не всегда доступны. Что бы как то управлять таким хранилищем вполне возможно понадобится какой то дополнительный софт.
  2. База данных специального сайта с CMS типа Joomla. Это хорошее рабочее решение. Данные защищены и доступны отовсюду. С сайта можно легко отправить почту. Однако есть и существенный недостаток. Для взаимодействия с таким сайтом понадобится разрабатывать дополнение, возможно объемное и, возможно, содержащее ошибки и дыры в безопасности. Другими словами, будет необходима разработка надежной инфраструктуры.
  3. Воспользоваться имеющимся готовым вариантом — сервисами Google. Там можно безопасно хранить контакты и иметь доступ к ним отовсюду и с разных устройств. Контактами, хранящимися на аккаунте Google,  можно легко управлять. Можно создавать разные списки (группы). Можно отправлять почту. Это все, что надо для комфортной работы. Поэтому для работы выбираем именно Google.

Про то, как взаимодействовать с Google, уже не мало написано, хотя бы здесь, что окажет нам существенную помощь. Для начала работы с Google нужно зарегистрировать аккаунт и создать там список контактов. В этом списке должны быть в том числе и те, по адресам которых будет осуществляться рассылка. Нужно создать в контактах группу с определенным именем, например "Forex", и добавить в эту группу выбранные контакты. У каждого контакта имеется возможность сохранить множество разнообразных данных, которые впоследствии будут доступны, но, если все -таки пользователю будет нужно создать еще какое то дополнительное поле с данными, то такая возможность не просматривается, к сожалению. Нужно будет обходиться теми, которые имеются в наличии, что не должно причинять неудобств, т.к. их действительно много, и далее будет показано, как этим можно воспользоваться.

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

Работа на стороне Google

Предположим, что аккаунт у Google у нас уже есть. Тогда нужно продолжить  создание проекта, используя "консоль разработчика" в Google. Что это такое, как пользоваться консолью и как создавать свой проект, детально, по шагам описано многократно, можно посмотреть например здесь. Поэтому повторять это описание еще раз смысла нет. Конечно, в статье по приведенной ссылке описывается другой проект. В нашем проекте нужно свое имя, выберем " WorkWithPeople". Кроме того, там речь идет о диске, который здесь не нужен. Нужны другие сервисы, поэтому на том этапе, когда необходимо их подключение к проекту, подключить следует следующие:

Первый сервис дает доступ к списку контактов (и не только, но в данном случае достаточно только доступа к списку). Имеется еще один сервис для доступа к списку контактов — Contacts API, но в настоящее время он не рекомендован к использованию, поэтому на него внимания не обращаем.

Второй сервис дает доступ к почте, как видно по его названию.

Подключаем сервисы и получаем ключи, которые дадут приложению доступ к этим сервисам. Записывать или пытаться запомнить их не надо. Нужно скачать предлагаемый файл в формате json, где будет вся необходимая информация для доступа к ресурсам Google, в т.ч. и эти ключи. Сохраняем файл у себя на диске, возможно дав ему более осмысленное название. У меня он назван "WorkWithPeople_gmail.json". На этом работа непосредственно с Google закончена. Аккаунт есть, список контактов есть, группа, для которой будет выполняться рассылка создана в списке контактов, проект создан и файл для доступа получен.

Теперь переходим к работе с VS 2017.

Проект и пакеты

Открываем VS 2017 и создаем стандартный проект Class Library (.NET Framework). Даем ему любое запоминаемое имя, у меня оно совпадает с именем проекта в Google (" WorkWithPeople"), но это не обязательно. Сразу устанавливаем дополнительные пакеты, используя NuGet:

В процессе установки NuGet предложит установить связанные пакеты, на что следует согласиться. В данном случае в проект устанавливаются пакеты от Google для работы с контактами, почтой и пакет для работы непосредственно с почтовыми отправлениями. Теперь все готово к написанию собственно кода.

Обращение к контакту

Начнем со вспомогательного класса. Если посмотреть на то, сколько информации содержит конкретный контакт на Google, то становится очевидным, что основная часть этой информации для решения данной конкретной задачи не нужна. Нужно имя контакта, чтобы можно было обратиться, и почтовый адрес для посылки письма. На самом деле нужны данные еще одного поля, но об этом позже.

Соответствующий класс может выглядеть так:

namespace WorkWithPeople
{    
    internal sealed class OneContact
    {
        public OneContact(string n, string e)
        {
            this.Name  = n;
            this.Email = e;
        }
        public string Name  { get; set; }
        public string Email { get; set; }
    }
}

Имеются два свойства типа "строка", хранящих имя контакта и его адрес, а так же элементарный конструктор с двумя параметрами для инициализации этих свойств. Никаких дополнительных проверок в классе не предусматривается, они будут выполняться в другом месте.

Простейшая сущность, список которых создается при чтении списка контактов. После чего выполнять рассылку можно будет опираясь на данные уже этого вновь построенного списка. В том случае, если необходимо будет этот список обновить, то мы удалим все элементы списка и повторим операцию чтения и выборки данных из аккаунта Google.

И еще один вспомогательный класс. В списках контактов вполне могут быть занесены неверные почтовые адреса, или их может не оказаться вовсе. Нам же, прежде чем посылать почту, желательно убедиться, что адрес имеется и он "имеет правильный вид". Для этого создаем еще один вспомогательный класс:

namespace WorkWithPeople
{    
    internal static class ValidEmail
    {
        public stati cbool IsValidEmail(this string source) => !string.IsNullOrEmpty(source) && new System.ComponentModel.DataAnnotations.EmailAddressAttribute().IsValid(source);
    }
}

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

Доступ и работа с сервисами

Нами уже открыт проект, получены ключи и скачан файл формата JSON для авторизации приложения. Поэтому создадим новый класс ContactsPeople и добавим в файл соответствующие сборки:

using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using System.Net.Mail;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Google.Apis.Auth.OAuth2;
using Google.Apis.People.v1;
using Google.Apis.Services;
using Google.Apis.Util.Store;
using Google.Apis.Http;
using Google.Apis.PeopleService.v1;
using Google.Apis.PeopleService.v1.Data;
using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data;

namespace WorkWithPeople
{
    internal sealed class ContactsPeople
    {
       public static string Applicationname { get; } = "WorkWithPeople";

.....

Сразу же добавляем статическое свойство, содержащее имя Google проекта.  Это статическое свойство сделано "только для чтения".

Добавим в класс закрытые поля и перечисление:

        private enum             PersonStatus
        {
            Active,
            Passive
        };
        private string           _groupsresourcename;
        private List<OneContact> _list = new List<OneContact>();
        private UserCredential   _credential;
        private PeopleService    _pservice;
        private GmailService     _gservice;

Перечисление будет служить для отметки контакта как "активного", и в этом случае ему направляется рассылка, и "пассивного". В последнем случае рассылка контакту не направляется. Прочие закрытые поля:

Начнем писать код основной рабочей функции:

        publicint WorkWithGoogle(string credentialfile, 
                                   string user, 
                                   string filedatastore, 
                                   string groupname,
                                   string subject,
                                   string body,
                                   bool   isHtml,
                                   List<string> attach = null)
        {
          ...

Её аргументы:

Возвращаемое значение — количество отправленных писем. Начнем писать код функции:

            if (!File.Exists(credentialfile))
                throw (new FileNotFoundException("Not found: " + credentialfile));
            using (var stream = new FileStream(credentialfile, FileMode.Open, FileAccess.Read))
            {
                if (_credential == null) {
                    _credential = GoogleWebAuthorizationBroker.AuthorizeAsync(
                        GoogleClientSecrets.Load(stream).Secrets,
                        new[]
                        {
                            GmailService.Scope.GmailSend,
                            PeopleService.Scope.ContactsReadonly
                        },
                        user,
                        CancellationToken.None,
                        new FileDataStore(filedatastore)).Result;
                        CreateServicies();
                }
                else if (_credential.Token.IsExpired(Google.Apis.Util.SystemClock.Default)) {
                    bool refreshResult = _credential.RefreshTokenAsync(CancellationToken.None).Result;
                    _list.Clear();
                    if (!refreshResult) return 0;   
                    CreateServicies();
                }
                
            }// using (var stream = new FileStream(credentialfile, FileMode.Open, FileAccess.Read))

Следует обратить внимание на массив строк, определяющих доступ к какому именно сервису запрашивается:

Кроме того обращает на себя внимание вызов GoogleWebAuthorizationBroker.AuthorizeAsync, имя которого подсказывает нам, что вызов будет выполнен асинхронно.

Обратите внимание, что если токен, полученный ранее, оказался просроченным, то код обновляет его и удаляет из ранее сформированного списка контактов _list все объекты.

Вспомогательная функция CreateServicies() создает и инициализирует необходимые объекты:

        private void         CreateServicies()
        {
            _pservice = new PeopleService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = _credential,
                ApplicationName = Applicationname
            });
            _gservice = new GmailService(new BaseClientService.Initializer()
            {
                HttpClientInitializer = _credential,
                ApplicationName = Applicationname
            });
        }

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

 - Используя JSON - файл с данными, сначала запрашиваем "полномочия" и сохраняем их в поле _credential. Затем вызываем конструкторы сервисов, передавая им в качестве инициализирующего списка поле с "полномочиями" и поле с именем проекта.

Настала пора получить список контактов выбранной для рассылки группы:

            try {
                  if (_list.Count == 0)
                    GetPeople(_pservice, null, groupname);                     
            }
            catch (Exception ex) {
                ex.Data.Add("call GetPeople: ", ex.Message);
                throw;
            }
#if DEBUG
            int i = 1;
            foreach (var nm in _list) {
                Console.WriteLine("{0} {1} {2}", i++, nm.Name, nm.Email);
            }
#endif
            if (_list.Count == 0) {
                Console.WriteLine("Sorry, List is empty...");
                return 0;
            }

Список _list, хранящий контакты будет заполнять функция GetPeople(...), к рассмотрению которой мы перейдем позже. Эта функция может служить источником исключения, поэтому её вызов обернут в блок try. Каких-то своих типов исключений в подключенных сборках не просматривается, поэтому блок catch написан в самом общем виде. А это в свою очередь означает, что не нужно пытаться поглотить абсолютно все, что может случиться, в данном месте, чтобы не потерять ценные данные для отладки. Поэтому добавляем ту информацию, которую считаем нужным, к исключению и возбуждаем его повторно.

Особо отметим, что список _list обновляется только тогда, когда он пустой, т.е. когда был получен новый токен, либо обновлен старый.

Следующий блок будет выполняться только для отладочной версии приложения. В нем просто выводится на консоль весь построенный список.

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

Функция заканчивается блоком кода формирования почтового отправления и выполнением рассылки:

            using (MailMessage mail = new MailMessage
            {
                Subject = subject,
                Body = body,
                IsBodyHtml = isHtml
            })  // MailMessage mail = new MailMessage
            {
                if (attach != null)
                {
                    foreach (var path in attach)
                        mail.Attachments.Add(new Attachment(path));
                } //  if (attach != null)

                foreach (var nm in _list)
                    mail.To.Add(new MailAddress(nm.Email, nm.Name));
                try
                {
                    SendOneEmail(_gservice, mail);
                }
                catch (Exception ex)
                {
                    ex.Data.Add("call SendOneEmail: ", ex.Message);
                    throw;
                }
            }// using (MailMessage mail = new MailMessage

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

Рассылку выполняет  функция SendOneEmail(...), которая будет рассмотрена в дальнейшем. Она, как и функция GetPeople(...), так же может оказаться источником исключения. Поэтому и её вызов так же обернут в блок try и обработка в catch так же выполнена аналогично.

На этом работа основной функции WorkWithGoogle(...) считается завершенной и она возвращает значение _list.Count, считая, что почтовые сообщения были направлены каждому контакту из списка.

Заполнение списка контактов

После получения доступа можно заполнить список _list. Это делает функция:

        private void         GetPeople(PeopleService service, string pageToken, string groupName)
        {
           ...

Её аргументы:

В первый раз функция вызывается со значением pageToken = NULL. Если запрос к Google впоследствии вернет этот токен со значением отличным от NULL, то функия будет вызвана рекурсивно.

            if (string.IsNullOrEmpty(_groupsresourcename))
            {
                ContactGroupsResource groupsResource = new ContactGroupsResource(service);
                ContactGroupsResource.ListRequest listRequest = groupsResource.List();
                ListContactGroupsResponse response = listRequest.Execute();
                _groupsresourcename = (from gr in response.ContactGroups
                                       where string.Equals(groupName.ToUpperInvariant(), gr.FormattedName.ToUpperInvariant())
                                       select gr.ResourceName).Single();
                if (string.IsNullOrEmpty(_groupsresourcename))
                    throw (new MissingFieldException($"Can't find GroupName: {groupName}"));
            }// if (string.IsNullOrEmpty(_groupsresourcename))

Выясняем имя ресурса по имени группы. Для этого запрашиваем список всех ресурсов, а потом  узнаем нужное в простом лямбда выражении. Обратите внимание, что ресурс с требуемым именем должен быть только один. Если ресурс в результате работы так и не был найден, возбуждается исключение.

Google.Apis.PeopleService.v1.PeopleResource.ConnectionsResource.ListRequest peopleRequest =
                new Google.Apis.PeopleService.v1.PeopleResource.ConnectionsResource.ListRequest(service, "people/me")
                {
                    PersonFields = "names,emailAddresses,memberships,biographies"
                };
            if (pageToken != null) {
                peopleRequest.PageToken = pageToken;
            }

Конструируем запрос к Google для получения нужного списка. Для этого в запросе указываем те поля, из всего множества данных контакта в Google, которые нас интересуют:

И, наконец, выполняем запрос:

            var request = peopleRequest.Execute();
            var list1 = from person in request.Connections
                     where person.Biographies != null
                     from mem in person.Memberships
                     where string.Equals(_groupsresourcename, mem.ContactGroupMembership.ContactGroupResourceName) &&
                           PersonActive(person.Biographies.FirstOrDefault()?.Value) == PersonStatus.Active
                     let name = person.Names.First().DisplayName
                     orderby name
                     let email = person.EmailAddresses?.FirstOrDefault(p => p.Value.IsValidEmail())?.Value
                     where !string.IsNullOrEmpty(email)
                     select new OneContact(name, email);
            _list.AddRange(list1);
            if (request.NextPageToken != null) {
                GetPeople(service, request.NextPageToken, groupName);
            }
        }//void GetPeople(PeopleService service, string pageToken, string groupName)

Делаем запрос и отфильтровываем нужные данные в лямбда выражении, выглядящем устрашающе, но, по сути, несложном. У контакта должна быть ненулевая биография, он должен состоять в нужной группе, он должен быть активным контактом и у него должен быть правильный адрес. Продемонстрируем здесь функцию, которая определяет статус "активный / пассивный" отдельного контакта по содержимому поля "biographies":

        private PersonStatus PersonActive(string value)
        {
            try {
                switch (Int32.Parse(value))
                {
                    case 1:
                        return PersonStatus.Active;
                    default:
                        return PersonStatus.Passive;
                }
            }
            catch (FormatException)   { return PersonStatus.Passive; }
            catch (OverflowException) { return PersonStatus.Passive; }
        }//PersonStatus PersonActive(string value)

Это единственная функция в проекте, которая не стремится повторно возбудить исключения, а пытается обработать некоторые из них на месте.

Все! Добавляем полученный список к списку _list. Если еще не все контакты прочитаны, вызываем функцию рекурсивно, с новым значением токена.

Отсылка почты

Этим занимается небольшая вспомогательная функция:

        private void SendOneEmail(GmailService service, MailMessage mail)
        {
            MimeKit.MimeMessage mimeMessage = MimeKit.MimeMessage.CreateFromMailMessage(mail);
            var encodedText = Base64UrlEncode(mimeMessage.ToString());
            var message = new Message { Raw = encodedText };

            var request = service.Users.Messages.Send(message, "me").Execute();
        }//  bool SendOneEmail(GmailService service, MailMessage mail)

Её вызов описан выше, поэтому повторяться не станем. Задача этой функции подготовить почту к отправке и выполнить её. Помимо этого в функции сконцентрированы все "тяжелые" операции по подготовке почтового отправлекния. К сожалению, Google не принимает данные в виде класса  MailMessage. Поэтому готовим данные в приемлемой форме и кодируем их. В состав сборки MimeKit входят инструменты, которые выполняют кодирование, но показалось проще использовать более простую функцию, имеющуюся в наличии. Она не сложная и отдельно в статье демонстрироваться не будет. Обратите внимание на специализировааный userId типа string в вызове service.Users.Messages.Send. Он равен специальному значению "me", что дает возможность Google обращаться к вашему аккаунту за данными об отправителе.

На этом мы заканчиваем разбор класса ContactsPeople. Прочие оставшиеся функции носят второстепенный, вспомогательный характер и останавливаться на них не будем.

Коннектор к терминалу

Нам осталось рассмотреть вопрос о сопряжении между нашей, еще не созданной, сборкой и терминалом. С первого взгляда задача не выглядит сложной. Определяем несколько статических методов, компилируем проект, копируем в папку "Libraries" терминала. Вызываем статические методы сборки из кода на MQL. Но что именно нужно скопировать? Есть наша сборка в виде библиотеки dll. Есть еще с десяток сборок, которые подгрузил NuGet и которые используются в работе. Есть файл в формате JSON, в котором находятся данные для доступа в Google. Попытаемся весь этот набор скопировать в папку "Libraries". Создадим примитивнейший скрипт на MQL (даже код этого скрипта прикладывать не стоит) и попытаемся вызвать какой-либо статический метод из нашей сборки. Исключение! Не найден файл Google.Apis.dll. Это очень неприятный сюрприз, означающий, что CLR не находит нужной сборки, хотя она лежит в той же папке, что и основная наша сборка. Почему это происходит? Подробно разбирать ситуацию здесь не стоит, а интересующихся подробностями я переадресую к известной книге Рихтера, в раздел про поиск приватных сборок.

Уже есть много примеров вполне работоспособных .Net приложений, работающих с MetaTrader-ом, там также возникали такие проблемы, как они решались? Например, вот здесь проблема решалась созданием канала между .Net приложением и программой на MQL. А здесь использовалась модель, основанная на событиях. Можно предложить похожий подход с передачей необходимых данных от программы на MQL до .Net приложения с помощью командной строки.

Но стоит рассмотреть способ, который представляется более "элегантным" и, вообще-то, не только более простым, но и более универсальным. Речь идет об управлении загрузкой сборок с помощью события AppDomain.AssemblyResolve. Это событие возникает, когда среда выполнения не может связать сборку по имени. Тогда обработчик события может загрузить и вернуть сборку из другой папки, адрес которой ему известен. Поэтому напрашивается достаточно красивое решение:

  1. Создаем в папке "Libraries" папку с другим именем, в моем случае "WorkWithPeople".
  2. Саму сборку, методы которой должны быть импортированы в файл с MQL, копируем в папку "Libraries", как и должно быть.
  3. Все прочие сборки проекта, включая файл типа JSON с информацией по доступу к сервисам Google, копируем в папку "WorkWithPeople".
  4. Сообщаем нашей главной сборке в папке "Libraries" адрес, где она должна будет искать прочие сборки — полный путь к папке "WorkWithPeople".

В результате получаем работоспособное решение и не засоряем папку "Libraries". Остается воплотить принятые решения в коде:

Класс управления

Создадим статический класс:

    public static class Run
    {

        static Run() {
            AppDomain.CurrentDomain.AssemblyResolve += ResolveAssembly;
        }// Run()

Создадим статический конструктор и в него добавим обработчик вышеназванного события, чтобы он появился в цепи обработчиков как можно раньше. Определим сам обработчик:

        static Assembly ResolveAssembly(object sender, ResolveEventArgs args) {
            String dllName = new AssemblyName(args.Name).Name + ".dll";
            return Assembly.LoadFile(Path.Combine(_path, dllName) );
        }// static Assembly ResolveAssembly(object sender, ResolveEventArgs args)

Теперь всякий раз, когда сборка не будет обнаружена, будет вызываться этот разработчик. Его задача загрузить и вернуть сборку, комбинируя путь из переменной _path (определяется при инициализации) и вычисляемое имя. Теперь исключение будет возникать только в том случае, если обработчик не сможет найти сборку.

Теперь функция инициализации:

public static void Initialize(string Path, string GoogleGroup, string AdminEmail, string Storage)
        {
            if (string.IsNullOrEmpty(Path) ||
                string.IsNullOrEmpty(GoogleGroup) ||
                string.IsNullOrEmpty(AdminEmail) ||
                string.IsNullOrEmpty(Storage)) throw (new MissingFieldException("Initialize: bad parameters"));
            _group = GoogleGroup;
            _user = AdminEmail;
            _storage = Storage;
            _path = Path;
        }//  Initialize(string Path, string GoogleGroup, string AdminEmail, string Storage)

Эта функция должна быть вызвана самой первой, обязательно ДО попытки выполнить рассылку. Её аргументы:

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

Для присоединенных файлов создаем список и простую функцию добавления:

public static void AddAttachment (string attach) { _attachList.Add(attach);}

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

Сразу же создаем объект для рассылки:

static ContactsPeople _cContactsPeople = new ContactsPeople();

И выполняем её вызовом функции:

public static int DoWork(string subject, string body, bool isHtml = false) {
            if (string.IsNullOrEmpty(body))
                throw (new MissingFieldException("Email body null or empty"));
            int res = 0;
            if (_attachList.Count > 0) {
                res = _cContactsPeople.WorkWithGoogle(Path.Combine(_path, "WorkWithPeople_gmail.json"),
                    _user,
                    _storage,
                    _group,
                    subject,
                    body,
                    isHtml,
                    _attachList);
                _attachList.Clear();
            } else {
                res = _cContactsPeople.WorkWithGoogle(Path.Combine(_path, "WorkWithPeople_gmail.json"),
                    _user,
                    _storage,
                    _group,
                    subject,
                    body,
                    isHtml);
            }// if (_attachList.Count > 0) ... else ...
            return res;
        }// static int DoWork(string subject, string body, bool isHtml = false)

Входные параметры следующие:

В зависимости от того, имеются ли вложения в письмо, или их нет, предусмотрены два варианта вызова _cContactsPeople.WorkWithGoogle. Интересен первый аргумент вызова:

Path.Combine(_path, "WorkWithPeople_gmail.json")

Это полный путь к файлу, содержащему информацию для доступа к сервисам Google.

Функция DoWork(...) возвращает количество отправленных писем.

Весь проект для VS++ 2017, за исключением файла с данными по доступу к Google находятся в прилагаемом архиве google.zip.

На стороне MetaTrader

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

#import "WorkWithPeople.dll"


void OnStart()
  {
   string scr = "scr.gif";
   string fl = TerminalInfoString(TERMINAL_DATA_PATH) + "\\MQL5\\Files\\";
   ChartScreenShot(0, scr, 800, 600);  
   Run::Initialize("e:\\Forex\\RoboForex MT5 Demo\\MQL5\\Libraries\\WorkWithPeople\\" ,"Forex" ,"ХХХХХХ@gmail.com" ,"WorkWithPeople" );
   Run::AddAttachment(fl + scr);
   int res = Run::DoWork("some subj" ,
                         "Very big body" ,
                          false );
   Print("result: ", res);   
  }

Код вполне очевиден. Импортируем сборку, первое, что делаем - инициализируем её, как говорилось выше, присоединяем сделанный ранее скриншот и выполняем рассылку. Полностью код содержится в прилагаемом файле google_test1.mq5.

Еще один пример — индикатор, работящий на таймфрейме M5 и отсылающий письмо со скриншотом всякий раз, когда обнаруживает новую свечу:

#import "WorkWithPeople.dll"

input string scr="scr.gif";

string fp;

int OnInit()
  {
   fp=TerminalInfoString(TERMINAL_DATA_PATH)+"\\MQL5\\Files\\";
   Run::Initialize("e:\\Forex\\RoboForex MT5 Demo\\MQL5\\Libraries\\WorkWithPeople\\","Forex","0ndrei1960@gmail.com","WorkWithPeople");

   return(INIT_SUCCEEDED);
  }

int OnCalculate(const int rates_total,
                const int prev_calculated,
                const datetime &time[],
                const double &open[],
                const double &high[],
                const double &low[],
                const double &close[],
                const long &tick_volume[],
                const long &volume[],
                const int &spread[])
  {
   if(IsNewCandle()) 
     {
      ChartScreenShot(0,scr,800,600);
      Run::AddAttachment(fp+scr);
      string body="Time: "+TimeToString(TimeLocal());
      int res=Run::DoWork("some subj",body,false);
      Print(body);
     }
   return(rates_total);  
  }

Полностью код данного индикатора находится в прилагаемом файле google_test2.mq5. Он настолько прост, что в комментариях не нуждается.

Заключение

На этом подведем некоторые итоги. Мы рассмотрели способ использования контактов Google для организации взаимодействия с партнерами. Кроме того, рассмотрен способ интеграции сборок с терминалом, позволяющий не засорять папки посторонними файлами. Стоит сказать немного об эффективности кода сборки. В данном случае этому вопросу не было уделено достаточно большого внимания, как второстепенному, не имеющему большого значения. Но можно предложить комплекс некоторых мероприятий в этом направлении:

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

В заключении стоит вернуться к вопросу об использовании средств MQL для данной задачи. Возможно ли это? Судя по документации Google, ответ положительный. Возможно добиться тех же результатов, используя GET/POST запросы, примеры которых имеются. А следовательно, можно будет использовать штатную WebRequest. Стоит ли это делать -—вопрос открытый, т.к. ввиду весьма большого количества запросов писать, отлаживать и сопровождать такой код будет достаточно непросто.

Программы, используемые в статье:

 # Имя
Тип
 Описание
1 google_test1.mq5
Скрипт
Скрипт, создающий скриншот и отсылающий его по нескольким адресам.
2
google_test1.mq5 Индикатор
Пример индикатора, отсылающего почту при каждой новой свече
3 google.zip Архив Проект сборки и тестового консольного приложения.