Creando una lista de correo electrónico por medio de los servicios Google

Andrei Novichkov | 20 agosto, 2019

Introducción

El trader que mantiene relaciones comerciales con otros traders, suscriptores, clientes o incluso con los amigos puede necesitar crear una lista de correo. Enviar las capturas de pantalla, revistas, registros o informes son tareas bastante relevantes que nos necesarias cada día, pero tampoco son tan raras. En cualquier caso, a algunos traders les gustaría disponer de esta posibilidad. Es definitivamente difícil, o incluso simplemente imposible, usar aquí los recursos estándar de MQL. Al final del artículo, volveremos a la cuestión de usar sólo MQL para resolver este problema. Mientras tanto, a parte de MQL, vamos a usar C# también. Eso nos permitirá escribir el código necesario con relativa facilidad y conectarlo al terminal, pero también nos planteará una tarea muy interesante relacionada con esta conexión.

El artículo está destinado para los desarrolladores de nivel inicial e intermedio que desean profundizar sus conocimientos en las cuestiones de la creación de las bibliotecas, su combinación con el terminal, y conocer los servicios de Google.

Planteamiento del problema

Ahora, definimos más claramente qué vamos a hacer y de qué manera. Hay una lista de contactos (desde luego es actualizable) que se usa para enviar un mensaje de correo, tal vez, con anexos, una vez o repetidamente, a cada uno de los contactos de esta lista. Hay que tomar en cuenta lo siguiente:

En seguida, nos enfrentamos con la cuestión de cómo gestionar estas listas. ¿Qué opciones pueden haber?

  1. La base de datos ubicada en disco duro (o un archivo CSV) es inconveniente y no tiene seguridad suficiente. No siempre están disponibles. Para gestionar este repositorio de alguna manera, es muy probable que necesitemos usar algún software adicional.
  2. Una base de datos de un sitio web especial con CMS tipo Joomla puede ser una buena solución de trabajo, pues, los datos están protegidos y están disponibles de cualquier lugar. Además, Usted puede enviar fácilmente un e-mail de este sitio. No obstante, hay una importante desventaja. Para interactuar con este sitio, habrá que desarrollar una aplicación adicional, tal vez, sea de gran volumen, y quizá, tanga errores y lagunas en la seguridad. En otras palabras, habrá que desarrollar una infraestructura confiable.
  3. Usar la opción disponible: servicios de Google. Ellos permiten almacenar los contactos de forma segura, pudiendo acceder a ellos de cualquier lugar y de cualquier dispositivo. Los contactos almacenados pueden gestionarse muy fácilmente. En particular, Usted puede formar diferentes listas (grupos) y enviar e-mails. Eso es todo lo que necesita para un trabajo comfortable. Por eso, elegimos Google para trabajar.

Existe mucha información sobre la interacción con Google, por ejemplo, aquí, lo que nos ayudará mucho. Para empezar a trabajar con Google, hay que registrar una cuanta y crear ahí una lista de contactos. La lista debe incluir los contactos a los cuales va a enviar los mensajes de correo. En los contactos, hay que crear un grupo con un determinado nombre, por ejemplo, "Forex", y añadir los contactos seleccionados a este grupo. Cada contacto puede almacenar varios datos que serán disponibles posteriormente, pero si el usuario necesita crear un campo adicional con datos, lamentablemente, esta posibilidad no existe. Eso no debe causar inconvenientes porque hay muchos campos. A continuación, mostraremos cómo usarlos.

De esta manera, vamos a considerar que ya tenemos una idea de los medios que disponemos. Además, ya ha sido hecho un trabajo preliminar y se puede proceder a la solución de las principales tareas.

Trabajo en el lado de Google

Supongamos que ya disponemos de una cuenta en Google. Entonces, es necesario continuar la creación del proyecto, usando la «consola del desarrollador» en Google. Aquí Usted puede informarse detalladamente de qué se trata, cómo usar la consola y cómo crear su proyecto. Por eso, no tiene sentido repetir esta descripción aquí. Naturalmente, el artículo del enlace mencionado contiene la descripción de otro proyecto. Nuestro proyecto necesita un nombre, elegimos " WorkWithPeople". Además, ahí se trata de un disco que no vamos a necesitar aquí. Necesitamos otros servicios, por eso, cuando es necesario conectarlos al proyecto, usamos los siguientes:

El primer servicio permite acceder a la lista de contactos (y no sólo eso, pero en este caso, será suficiente simplemente acceder a la lista). Existe otro servicio para acceder a los contactos (contactos API), pero en este momento no está recomendado, por eso, no preste atención en él.

El segundo servicio proporciona el acceso al correo, en lo que indica su nombre.

Activamos los servicios y obtenemos las claves que permiten a la aplicación acceder a ellos. No es necesario anotar o memorizarlas. Hay que descargar el archivo adjunto en el formato json, donde va a almacenarse toda la información necesaria para acceder a los recursos de Google, inclusive, las claves. Guarde el archivo en el disco, tal vez, dándole un nombre más adecuado. En mi caso, es "WorkWithPeople_gmail.json". Aquí, el trabajo con Google está terminado. Tenemos una cuenta, lista de contactos, grupo para el cual va a realizarse el envío, proyecto y el archivo para el acceso.

Ahora, comenzamos a trabajar con VS 2017.

Proyecto y paquetes

Abrimos VS 2017 y creamos un proyectos estándar Class Library (.NET Framework). Le damos cualquier nombre fácil de memorizar, en mi caso, coincide con el nombre del proyecto en Google (" WorkWithPeople"), pero eso no es obligatorio. Instalamos los paquetes adicionales, usando NuGet:

En el proceso de la instalación, NuGet propone instalar los paquetes relacionados. Acéptelo. En este caso, el proyecto recibe los paquetes de Google para trabajar con los contactos, correo y un paquete para trabajar con envíos de correo. Ahora, tenemos todo preparado para escribir nuestro propio código.

Acceso al contacto

Empezamos con una clase auxiliar. Si echamos un vistazo a la cantidad de la información de un determinado contacto en Google, se hace obvio que la mayor parte de esta información no es necesaria para la solución de nuestra tarea. Necesitamos el nombre del contacto para acceder y la dirección de correo para enviar los mensajes. En realidad, necesitamos los datos de otro campo, pero de eso hablaremos más tarde.

La clase correspondiente puede tener esta apariencia:

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; }
    }
}

Hay dos propiedades tipo «cadena» (string) que almacenan el nombre del contacto y su dirección, así como, un constructor simple con dos parámetros para inicializar estas propiedades. No hay verificaciones adicionales en esta clase, ellas se realizan en otro lugar.

Una lista de elementos simples se crea cuando se lee la lista de contactos. Después de eso, podemos realizar nuestra campaña de correo basándose en los datos de esta lista recién creada. Si es necesario actualizar esta lista, eliminamos todos los elementos de la lista y repetimos la operación de la lectura y selección de los datos de la cuenta de Google.

Aquí tenemos otra clase auxiliar. Las listas de contactos pueden contener las direcciones de correo incorrectos o estar vacías. Antes de enviar los mensajes de correo, se recomienda que Usted esté seguro de que la dirección existe y es correcta. Para eso, creamos otra clase auxiliar:

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

Para realizar la verificación, usamos las herramientas disponibles, aunque también podemos usar expresiones regulares. Para la conveniencia del uso posterior, diseñamos el código como un método de extensión. Como no es difícil de adivinar, el método va a devolver true si la cadena que contiene la dirección de correo pasa la verificación, y false en caso contrario. Ahora podemos proceder a escribir la parte principal del código.

Acceso y trabajo con los servicios

Ya hemos abierto el proyecto, hemos obtenido las claves y hemos descargado el archivo con el formato JSON para autorizar la aplicación. por eso, vamos a crear una nueva clase ContactsPeople y añadimos ensamblados correspondientes al archivo:

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";

.....

Añadimos una propiedad estática que contiene el nombre del proyecto Google.  Esta propiedad estática ha sido hecha «solamente para lectura».

Añadimos los campos privados y una enumeración a la clase:

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

La enumeración va a servir para marcar un contacto como «activo» (en este caso, se le envían los mensajes), y «pasivo» (no recibe e-mail). Otros campos cerrados:

Vamos a empezar a escribir el código de la función principal:

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

Sus argumentos son:

Valor devuelto es el número de mensajes enviados. Vamos a empezar a escribir el código de la función:

            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))

Merece la pena prestar atención en el array de cadenas que determinan el acceso al servicio solicitado:

Además, observe la llamada a GoogleWebAuthorizationBroker.AuthorizeAsync, cuyo nombre indica que la llamada va a realizarse de manera asincrónica.

Obsérvese que si el token obtenido anteriormente está caducado, el código lo actualiza y elimina todos los objetos de la lista de contactos _list formada anteriormente.

La función auxiliar CreateServicies() crea e inicializa los objetos necesarios:

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

Como podemos ver, después de ejecutar los segmentos del código descritos anteriormente, obtenemos el acceso a los servicios necesarios:

Usando el archivo JSON con los datos, primero, solicitamos los «permisos» y los guardamos en el campo _credential. Luego, llamamos a los constructores de los servicios, pasándoles el campo con los «permisos» y el campo con el nombre del proyecto, como una lista de inicialización.

Ha llegado el momento para obtener la lista de contactos del grupo seleccionado para el envío:

            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;
            }

La lista _list, que guarda los contactos va a llenarse por la función GetPeople(...) que será considerada más tarde. Esta función puede servir de la fuente de la excepción, por eso, su llamada está envuelta en el bloque try. Ningunos tipos de excepciones no están detectadas en los ensamblajes activadas, por tanto, el bloque catch está escrito en forma muy general. En otras palabras, eso significa que no hay que intentar incluir absolutamente todas las ocurrencias posibles para no perder los datos valiosos para la depuración. Por eso, añadimos los datos que consideramos necesarios a la excepción y la reactivamos.

Tenga en mente que _list se actualiza sólo cuando ella está vacía, es decir, cuando se recibe un nuevo token o cuando ha sido actualizado el antiguo.

El siguiente bloque va a ejecutarse sólo para la versión de depuración de la aplicación. En él, la lista entera simplemente se visualiza en la consola.

El bloque final es bastante obvio. Si la lista está vacía, el siguiente trabajo no tiene sentido y se termina con la muestra de un mensaje.

La función se finaliza con un bloque del código para la formación del envío de correo y con la ejecución de la campaña de correo:

            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

Aquí, se crea una instancia de la clase MailMessage, con la inicialización posterior y el llenado de los campos. Si hay una lista de anexos, se añade. Finalmente, se forma la lista del envío obtenida en la fase anterior.

El envío se realiza por la función SendOneEmail(...) que será considerada más tarde. Igual como la función GetPeople(...), ella también puede ser una fuente de la excepción. Por eso, su llamada también está envuelta en el bloque try y el procesamiento en catch está hecha de forma semejante.

En este punto, el trabajo de la función WorkWithGoogle(...) se considera finalizado y ella devuelve el valor _list.Count, considerando que los mensajes de correo han sido enviados a cada contacto de la lista.

Llenando la lista de contactos

Después de obtener el acceso, se puede llenar la lista _list. Eso se hace por la función:

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

Sus argumentos son:

La primera vez, la función se invoca con el valor pageToken = NULL. Si luego la solicitud a Google devuelve este token con un valor diferente de NULL, la función será llamada recursivamente.

            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))

Averiguaremos el nombre del recurso por el nombre del grupo. Para eso, solicitamos la lista de todos los recursos, y luego, averiguamos el que necesitamos en una simple expresión lambda. Nótese que debe haber sólo un recurso con el nombre requerido. Si el recurso no ha sido encontrado, se activa la expresión.

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;
            }

Vamos a construir una solicitud a Google para obtener la lista necesaria. Para eso, especificamos sólo los campos de datos del contacto en Google en los que estamos interesados:

Finalmente, ejecutamos la solicitud:

            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)

Hacemos la solicitud y filtramos los datos necesarios en la expresión lambda que parece asustadora pero, en realidad, no es complicada. El contacto debe tener una biografía diferente de cero, debe estar en el grupo correcto, tiene que ser un contacto activo y tener dirección correcta. Vamos a mostrar una función que determina el estatus «activo/pasivo» de un contacto individual según el contenido del campo "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)

Es la única función en el proyecto que no procura activar de nuevo las excepciones, pero intenta procesar algunas de ellas localmente.

¡Eso es todo! Añadimos la lista obtenida a la lista _list. Si han sido leídos no todos los contactos, llamamos a la función de forma recursiva, con un nuevo valor del token.

Envío de e-meil

Una pequeña función auxiliar se encarga de eso:

        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)

Su llamada ha sido descrita antes, por eso, no vamos a repetirla. La tarea de esta función es preparar el envío de e-mail y ejecutarlo. A parte de eso, todas las operaciones «pesadas» para preparar el envío de correo están concentradas en esta función. Lamentablemente, Google no acepta los datos como la clase  MailMessage. Por eso, preparamos los datos en forma aceptable y los codificamos. El ensamblado MimeKit incluye las herramientas que ejecutan la codificación, pero parece más fácil usar una función más simple disponible. Ella no es complicada y no será mostrada separadamente en el artículo. Preste atención en userId especializado tipo string en la llamada service.Users.Messages.Send. Es igual al valor especial "me", que permite que Google acceda a su cuenta para obtener la información sobre el remitente.

Aquí, terminamos el análisis de la clase ContactsPeople. Las demás funciones tienen un carácter secundario, auxiliar y no vamos a analizarlas.

Conector al terminal

Nos queda considerar la cuestión de la conexión entre nuestro ensamblado (todavía no creado) y el terminal. A primera vista, esta tarea no parece difícil. Definimos varios métodos estáticos, compilamos el proyecto, copiamos a la carpeta "Libraries" del terminal. Llamamos los métodos estáticos del ensamblado desde el código en MQL. ¿Pero qué exactamente tenemos que copiar? Existe nuestro ensamblado en forma de una biblioteca dll. También hay una decena de ensamblados que han sido cargados por NuGet y que se usan en el trabajo. Hay un archivo en formato JSON que contiene los datos para el acceso en Google. Intentaremos copiar todo este conjunto a la carpeta "Libraries". Creamos un script primitivo en MQL (incluso no hay que adjuntar el código de este script) y intentaremos llamar a algún método estático de nuestro ensamblado. ¡Excepción! El archivo Google.Apis.dll no ha sido encontrado. Es una sorpresa muy desagradable que significa que CLR no encuentra el ensamblado necesario, aunque se encuentra en la misma carpeta que nuestro ensamblado principal. ¿Por qué ocurre eso? No vale la pena analizar la situación aquí. En vez de eso, voa a redireccionar a los interesados en los detalles al famoso libro de Ritchter, a la sección sobre la búsqueda de los ensamblados privados.

Hay muchos ejemplos de unas aplicaciones .Net totalmente funcionales que trabajan con MetaTrader, ahí también hay problemas, ¿cómo han sido resueltos? Por ejemplo, aquí el problema fue resuelto creando un canal entre la aplicación .Net y el programa en MQL. Mientras que aquí fue usado un modelo a base de los eventos. Se puede proponer un enfoque semejante con la transferencia de datos necesarios del programa MQL a la aplicación .Net usando la línea de comandos.

Pero merece la pena considerar un método que parece más «elegante» y no sólo más simple, pero también más universal. Se trata del control del cargamento de los ensamblados a través del evento AppDomain.AssemblyResolve. Este evento ocurre, cuando el ambiente de la ejecución no puede asociar el ensamblado por el nombre. En este caso, el manejador del evento puede cargar y devolver el ensamblado desde otra carpeta cuya dirección es conocida. Por tanto, surge una solución bastante bonita:

  1. Creamos la carpeta con otro nombre en la carpeta "Libraries" (en mi caso es "WorkWithPeople").
  2. Copiamos el propio ensamblado, cuyos métodos deben ser importados al archivo con MQL, a la carpeta la carpeta "Libraries", como debe ser.
  3. Todos los demás ensamblados del proyecto, inclusive el archivo tipo JSON con la información sobre el acceso a los servicios Google, copiamos a la carpeta "WorkWithPeople".
  4. Informamos a nuestro ensamblado principal en la carpeta "Libraries" la dirección donde tendrá que buscar otros ensamblados — la ruta completa a la carpeta "WorkWithPeople".

Como resultado, obtenemos una solución funcional y no ensuciamos la carpeta "Libraries". Nos queda implementar las decisiones tomadas en el código:

Clase de la gestión

Vamos a crear una clase estadística:

    public static class Run
    {

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

Vamos a crear un constructor estático e incluir un manejador del evento mencionado en él, par que él aparezca en la cadena de manejadores lo más pronto posible. Definimos el manejador:

        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)

Ahora, cada vez que no se detecte un ensamblado, se invocará este desarrollador. Su tarea es cargar y devolver el ensamblado, combinando la ruta desde la variable _path (se determina durante la inicialización) y el nombre calculado. Ahora, la excepción va a ocurrir sólo si el manejador no puede encontrar el ensamblado.

Ahora la función de inicialización:

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)

Esta función tiene que invocarse la primera, obligatoriamente ANTES del intento de la ejecución del envío. Sus argumentos son:

Todos los argumentos descritos no tienen que estar en blanco. De lo contrario, se activa la excepción.

Para los archivos adjuntos, creamos una lista y una función simple de adición:

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

Recordando que van a adjuntarse las capturas de pantalla o algunos otros archivos creados previamente en MetaTrader, la función no tiene previsto ningún medio de verificación en cuanto a la presencia o ausencia de los errores. Se supone que una herramienta de gestión que trabaja en el terminal se encargará de este trabajo.

Creamos inmediatamente un objeto para el envío:

static ContactsPeople _cContactsPeople = new ContactsPeople();

Ejecutamos llamando a la función:

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)

Los parámetros de entrada son los siguientes:

Dependiendo de que si haya archivos adjuntos o no, hay dos opciones de llamar a _cContactsPeople.WorkWithGoogle. El primer argumento de la llamada es interesante:

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

Es la ruta completa hacia el archivo con la información para acceder a los servicios de Google.

La función DoWork(...) devuelve el número de mensajes enviados.

El proyecto entero para VS++ 2017, a excepción del archivo con datos de acceso a Google, se encuentra en el archivo google.zip.

En el lado de MetaTrader

Después de terminar de revisar el código del ensamblado, vamos al lado del terminal y creamos un script elemental: Puede estar escrito aproximadamente así (omitiendo una parte del código al principio):

#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);   
  }

El código es bastante obvio. Importamos el ensamblado, la primera cosa que hacemos es inicializarlo como ha sido dicho antes, adjuntamos la captura de pantalla hecha anteriormente y ejecutamos el envío. El código completo se encuentra en el archivo google_test1.mq5.

Otro ejemplo es el indicador que trabaja en el timeframe M5 y que envía un mensaje con la captura de pantalla cada vez que detecte una vela nueva.

#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);  
  }

El código completo de este indicador se encuentra en el archivo adjunto google_test2.mq5. Es tan simple que no es necesario comentarlo.

Conclusiones

Vamos a resumir un poco. Hemos considerado una manera de usar los contactos de Google para organizar una interracción con los socios. Además, hemos considerado una manera de integrar los ensamblados con el terminal que permite no entupir las carpetas con archivos ajenos. Vale la pena hablar un poco sobre la eficacia del código del ensamblado. En este caso, a esta cuestión no le ha sido dada una atención suficiente, siendo ésta como secundaria y de poca importancia. Pero se puede proponer algunas actividades al respecto:

No voy a afirmar que es necesario usar todos los medios mencionados, pero es muy probable que su aplicación aumentará la interacción y permitirá usar un ensamblado obtenido no sólo con MetaTrader, sino también de forma independiente, como parte de un proceso separado.

En conclusión, merece la pena volver a la cuestión del uso de los medios de MQL para esta tarea. ¿Será posible eso? Según la documentación de Google, la respuesta es positiva. Es posible usar los mismos resultados usando las solicitudes GET/POST cuyos ejemplos existen. Por consiguiente, se puede usar WebRequest estándar. ¿Si merece la pena hacer eso? Es una pregunta abierta, porque debido a un gran número de solicitudes, será bastante difícil escribir, depurar y mantener este código.

Programas usados en el artículo:

 # Nombre
Tipo
 Descripción
1 google_test1.mq5
Script
Script que crea una captura de pantalla y la envía a varias direcciones.
2
google_test1.mq5 Indicador
Ejemplo del indicador que envía el correo con cada nueva vela.
3 google.zip Archivo Proyecto del ensamblado y de la aplicación de consola de texto.