Organisation einer Mailing-Kampagne mit den Google-Services

6 August 2019, 09:10
Andrei Novichkov
0
137

Einführung

Ein Händler kann eine Mailing-Kampagne organisieren, um Geschäftsbeziehungen zu anderen Händlern, Abonnenten, Kunden oder Freunden zu pflegen. Außerdem kann es notwendig sein, Screenshots, Protokolle oder Berichte zu senden. Dies sind vielleicht nicht die am häufigsten auftretenden Aufgaben, aber eine solche Möglichkeit ist eindeutig von Vorteil. Es wäre definitiv schwierig oder gar unmöglich, hier komfortable MQL-Tools zu verwenden. Am Ende des Artikels werden wir auf die Frage zurückkommen, ob wir zur Lösung dieser Aufgabe ausschließlich MQL-Tools verwenden. Bis dahin werden wir die Kombination aus MQL und C# verwenden. Dies wird es uns ermöglichen, den notwendigen Code relativ einfach zu schreiben und mit dem Terminal zu verbinden. Außerdem wird es auch eine sehr interessante Herausforderung im Zusammenhang mit dieser Verbindung darstellen.

Der Artikel richtet sich an Anfänger und mäßig fortgeschrittene Entwickler, die ihr Wissen über das Schreiben von Bibliotheken und deren Integration in das Terminal vertiefen und sich mit den Google-Diensten vertraut machen wollen.

Erklärung des Problems

Lassen Sie uns nun genauer definieren, was wir tun werden. Es gibt eine aktualisierbare Kontaktliste, mit der Nutzer E-Mails mit Anhängen einmalig oder wiederholt an jeden Kontakt aus der Liste senden können. Was es zu beachten gilt:

  • Einige Kontakte aus der Liste haben möglicherweise keine oder eine falsche Adresse. Außerdem kann es mehrere Adressen geben.
  • Die Liste kann geändert werden — Kontakte können hinzugefügt oder gelöscht werden.
  • Kontakte können dupliziert werden.
  • Sie können auch von Mailingaktionen ausgeschlossen werden, obwohl sie in der Liste bleiben. Mit anderen Worten, die Aktivität des Kontakts sollte einstellbar sein.
  • Darüber hinaus wird die Liste mit Sicherheit Kontakte enthalten, die nicht mit der jeweiligen Aufgabe zusammenhängen.

Die Implementierung der Listenverwaltung ist die vordringlichste Aufgabe. Welche Möglichkeiten haben wir hier?

  1. Eine HDD-Datenbank oder eine CSV-Datei ist unbequem und nicht zuverlässig genug. Sie ist nicht immer verfügbar, und es kann eine zusätzliche Software erforderlich sein, um einen solchen Speicher zu verwalten.
  2. Eine Datenbank einer speziellen Website mit dem Joomla-Typ CMS. Dies ist eine gut funktionierende Lösung. Die Daten sind geschützt und von überall zugänglich. Außerdem können E-Mails einfach von der Website aus versendet werden. Es gibt aber auch einen erheblichen Nachteil. Für die Interaktion mit einer solchen Website ist ein spezielles Add-on erforderlich. Ein solches Add-on kann recht groß sein und mit Sicherheitslücken behaftet sein. Mit anderen Worten, eine zuverlässige Infrastruktur ist hier ein Muss.
  3. Nutzung der vorgefertigten Google-Services. Dort können Sie Kontakte sicher speichern und verwalten sowie von verschiedenen Geräten aus darauf zugreifen. Insbesondere können Sie verschiedene Listen (Gruppen) bilden und E-Mails versenden. Das ist alles, was wir für ein angenehmes Arbeiten brauchen. Bleiben wir also bei dieser Option.

Die Interaktion mit Google ist ausführlich dokumentiert, z.B. hier. Um mit Google zu arbeiten, registrieren Sie ein Konto und erstellen Sie dort eine Liste der Kontakte. Die Liste sollte Kontakte enthalten, an die wir E-Mails senden sollen. Erstellen Sie in den Kontakten eine Gruppe mit einem bestimmten Namen, z.B. "Forex", und fügen Sie ausgewählte Kontakte hinzu. Jeder Kontakt ist in der Lage, mehrere Daten zu speichern, um später verfügbar zu sein. Wenn ein Benutzer noch ein zusätzliches Datenfeld benötigt, kann es leider nicht angelegt werden. Dies sollte keine Unannehmlichkeiten verursachen, da viele Datenfelder zur Verfügung stehen. Ich werde später zeigen, wie man sie benutzt.

Nun ist es an der Zeit, zu den Hauptaufgaben überzugehen.

Vorbereitungen auf der Google-Seite

Angenommen, wir haben bereits ein Google-Konto. Setzen Sie die Projektentwicklung mit der Google "Development-Konsole" fort. Hier können Sie im Detail erfahren, wie Sie die Konsole nutzen und ein Projekt entwickeln. Natürlich beschäftigt sich der Artikel, zu dem der obige Link führt, mit einem anderen Projekt. Unser Projekt braucht einen Namen. Nennen wir es " WorkWithPeople". Wir werden andere Dienstleistungen benötigen. Aktivieren Sie in diesem Stadium die Folgenden:

  • People API
  • Gmail API

Die Erste bietet Zugriff auf die Kontaktliste (tatsächlich bietet sie auch Zugriff auf andere Dinge, aber wir benötigen nur die Liste). Es gibt einen weiteren Dienst für den Zugriff auf die Kontaktliste — Contacts API, aber zur Zeit wird er nicht zur Verwendung empfohlen, so dass wir ihn nicht beachten.

Wie der Name schon sagt, ermöglicht der zweite Dienst den Zugriff auf E-Mails.

Aktivieren Sie die Dienste und Sie erhalten die Schlüssel, die der Anwendung Zugriff auf sie gewähren. Es ist nicht nötig, sie aufzuschreiben oder sich zu erinnern. Laden Sie die angehängte Datei im Json-Format herunter, die alle notwendigen Daten für den Zugriff auf Google-Ressourcen enthält, einschließlich dieser Schlüssel. Speichern Sie die Datei auf Ihrer Festplatte und geben Sie ihr vielleicht einen sprechenden Namen. In meinem Fall heißt sie "WorkWithPeople_gmail.json". Damit ist die direkte Arbeit mit Google abgeschlossen. Wir haben das Konto, die Kontaktliste und das Projekt erstellt, sowie die Zugangsdatei erhalten.

Kommen wir nun zur Arbeit mit VS 2017.

Projekt und Pakete

Öffnen Sie VS 2017 und erstellen Sie ein Standardprojekt Class Library (.NET Framework). Benennen Sie auch sie mit einem sprechenden Namen (in meinem Fall stimmt es mit dem Google-Projektnamen "WorkWithPeople" überein, obwohl dies nicht obligatorisch ist). Installieren Sie zusätzliche Pakete mit NuGet sofort:

  • Google.Apis
  • Google.Apis.People.v1
  • Google.Apis.PeopleService.v1
  • Google.Apis.Gmail.v1
  • MimeKit

    Während der Installation bietet NuGet an, zugehörige Pakete zu installieren. Stimmen Sie dem zu. In unserem Fall erhält das Projekt die Google-Pakete für die Arbeit mit Kontakten und die Verwaltung von E-Mails. Jetzt sind wir bereit, den Code zu entwickeln.

    Auf einen Kontakt zugreifen

    Beginnen wir mit der Hilfsklasse. Betrachtet man die Datenmenge, die ein bestimmter Google-Kontakt enthält, so wird deutlich, dass der Hauptteil davon für unsere Aufgabe nicht benötigt wird. Wir benötigen einen Kontaktnamen und eine Adresse, um eine E-Mail zu senden. Tatsächlich brauchen wir Daten aus einem anderen Bereich, aber dazu später mehr.

    Die entsprechende Klasse könnte wie folgt aussehen:

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

    Es gibt zwei Eigenschaften vom Typ "string", die den Kontaktnamen und die Adresse speichern, sowie einen einfachen Konstruktor mit zwei Parametern zur Initialisierung. Es sind keine zusätzlichen Prüfungen implementiert. Diese sind an anderer Stelle durchzuführen.

    Beim Lesen der Kontaktliste wird eine Liste einfacher Elemente erstellt. Dies ermöglicht es, eine Mailing-Kampagne auf Basis der Daten dieser neu erstellten Liste durchzuführen. Wenn Sie die Liste aktualisieren möchten, entfernen Sie alle Listenelemente und wiederholen Sie den Vorgang zum Lesen und Auswählen von Daten aus dem Google-Konto.

    Es gibt noch eine weitere Hilfsklasse. Die Kontaktliste kann eine ungültige E-Mail-Adresse enthalten oder überhaupt keine. Bevor wir eine E-Mail senden, müssen wir sicherstellen, dass die Adresse vorhanden und korrekt ist. Lassen Sie uns die neue Hilfsklasse entwickeln, um das zu erreichen:

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

    Um eine Prüfung durchzuführen, verwenden wir verfügbare Werkzeuge, obwohl wir auch reguläre Ausdrücke verwenden können. Um die weitere Verwendung zu erleichtern, entwickeln wir den Code als Erweiterungsmethode. Wie nicht schwer zu erraten, gibt die Methode true zurück, wenn eine Zeichenkette, die eine Postadresse enthält, die Prüfung besteht, ansonsten false. Jetzt ist es an der Zeit, zum Hauptteil des Codes überzugehen.

    Zugriff und Arbeit mit Diensten

    Wir haben das Projekt bereits erstellt, die Schlüssel erhalten und die JSON-Datei zur Autorisierung der Anwendung heruntergeladen. Erstellen wir also eine neue Klasse ContactsPeople und fügen die entsprechenden Baugruppen der Datei hinzu:

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

    Fügen wir die 'static' Eigenschaft mit dem Google-Projektnamen hinzu. Diese 'statisc' Eigenschaft wird schreibgeschützt ausgeführt.

    Fügen wir der Klasse noch die geschlossenen Felder und Enumerationen hinzu:

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

    Die Enumeration wird verwendet, um einen Kontakt als "active" (empfängt E-Mails) und "passive" (empfängt keine E-Mails) zu markieren. Andere geschlossene Felder:

    • _groupsresourcename. Google-Ressourcenname, der der Gruppe entspricht, die in den "contacts" erstellt wurde. (In unserem Fall war der ausgewählte Gruppenname "Forex").
    • _list. Die Liste der Kontakte, für die eine Mailing-Kampagne gelten soll.
    • _credential. Anwendung "powers".
    • _pservice, _gservice. Dienstleistungen für die Arbeit mit Kontakten und E-Mails.

    Schreiben wir den Code der Hauptfunktion:

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

    Deren Argumente sind:

    • credentialfile. Name und Pfad des Zugriffs auf die JSON-Datei mit allen Daten für den Zugriff auf die Dienste. Das wurde zuvor aus dem Google-Konto heruntergeladen.
    • user. Google-Konto-Name — Adresse XXXXX@gmail.com.
    • filedatastore. Name eines Hilfsordners — Speicherung auf dem PC eines Benutzers (kann beliebig sein). Der Ordner wird innerhalb von AppData angelegt (%APPDATA%) und enthält die Datei mit zusätzlichen Zugriffsdaten.
    • groupname. Name einer Kontaktgruppe für eine von uns erstellte Mailing-Kampagne. In unserem Fall ist es "Forex".
    • subject, body, isHtml. Betreff und Text der E-Mail und ob sie im HTML-Format geschrieben ist.
    • attach. Liste der angehängten Dateien.

    Rückgabewert — Anzahl der gesendeten E-Mails. Beginnen wir mit dem Schreiben des Funktionscodes:

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

    Achten Sie auf die Anordnung der Zeilen, die den Zugriff auf den anzufordernden Dienst definieren:

    • GmailService.Scope.GmailSend. Dies ist ein Zugriff zum Versenden von E-Mails.
    • PeopleService.Scope.ContactsReadonly. Zugriff auf Kontakte im Nur-Lese-Modus.

    Außerdem ist der Aufruf von GoogleWebAuthorizationBroker.AuthorizeAsync zu beachten. Der Name deutet darauf hin, dass der Aufruf asynchron erfolgen soll.

    Beachten Sie, dass, wenn ein zuvor empfangenes Token überfällig ist, der Code es aktualisiert und alle Objekte aus der zuvor erstellten _list entfernt.

    Die Hilfsfunktion CreateServicies() erstellt und initialisiert die notwendigen Objekte:

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

    Wie man sieht, erhalten wir Zugang zu den notwendigen Diensten, nachdem wir die oben dargestellten Codesegmente ausgeführt haben:

     - Mit Hilfe der JSON-Datendatei fordern wir zunächst die "powers" an und speichern sie im Feld _credential. Dann rufen wir die Service-Konstruktoren auf, der die Felder "power" und den Projektnamen als Initialisierungsliste übergeben werden.

    Es ist an der Zeit, die Kontaktliste einer Gruppe zu erhalten, die für eine Mailing-Kampagne ausgewählt wurde:

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

    Die Funktion GetPeople(...) (siehe weiter unten) soll _list zum Speichern der Kontakte ausfüllen. Diese Funktion dient als Fehlerursache (exception), daher wird ihr Block in den Block try geschrieben. In verbundenen Gruppen werden keine Fehlertypen erkannt, daher wird der Block catch in der allgemeinsten Form geschrieben. Mit anderen Worten, wir müssen hier nicht alle möglichen Ereignisse einbeziehen, um nicht wertvolle Daten für das Debugging zu verlieren. Fügen wir daher den Daten die Fehler hinzu, die wir für notwendig halten, und aktivieren sie erneut.

    Man beachte, dass _list nur aktualisiert wird, wenn es leer ist, d.h. wenn es ein neues Token erhält oder das alte aktualisiert wurde.

    Der nächste Block wird nur für die Debugging-Version ausgeführt. Die gesamte erstellte Liste wird einfach in der Konsole angezeigt.

    Der letzte Block ist ganz offensichtlich. Bleibt die Liste leer, hat die weitere Arbeit keinen Sinn und wird mit der entsprechenden Meldung gestoppt.

    Die Funktion endet mit dem Codeblock, der eine ausgehende E-Mail bildet und eine Mailing-Kampagne durchführt:

                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
    

    Hier wird eine Instanz der Bibliotheksklasse MailMessage erzeugt. Anschließend folgt die Initialisierung und das Ausfüllen der Felder. Die Liste der Anlagen wird hinzugefügt, falls vorhanden. Schließlich wird die in der vorherigen Phase erhaltene Mailingliste gebildet.

    Das Versenden wird mit der Funktion SendOneEmail(....) durchgeführt, die später beschrieben werden soll. Genau wie die Funktion GetPeople(...) kann sie auch zu einer Ursache für einen Fehler werden. Daher wird ihr Aufruf auch in den Block try eingepackt, und die Handhabung in catch erfolgt ähnlich.

    An dieser Stelle wird die Arbeit der Hauptfunktion WorkWithGoogle(...) als abgeschlossen betrachtet und sie gibt den Wert _list.Count zurück, vorausgesetzt, dass E-Mail-Nachrichten an jeden Kontakt aus der Liste gesendet wurden.

    Ausfüllen der Kontaktliste

    Nach dem Zugriff ist _list bereit zum Ausfüllen. Dies geschieht durch die Funktion:

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

    Deren Argumente sind:

    • service. Ein Link zu einer zuvor erstellten Zugriffsklasse auf Google-Kontakte.
    • pageToken. Es können mehrere Kontakte vorhanden sein. Dieses Argument sagt dem Entwickler, dass die Liste der Kontakte mehrere Seiten umfasst.
    • groupName. Name einer Kontaktgruppe, an der wir interessiert sind.

    Beim ersten Mal wird die Funktion mit pageToken = NULL aufgerufen. Wenn eine Anfrage an Google anschließend das Token mit dem von NULL verschiedenen Wert zurückgibt, wird die Funktion rekursiv aufgerufen.

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

    Wir müssen den Namen einer Ressource über einen Gruppennamen herausfinden. Um dies zu erreichen, fordern Sie die Liste aller Ressourcen an und finden die notwendige in einem einfachen Lambda-Ausdruck heraus. Man beachte, dass es nur eine Ressource mit dem gewünschten Namen geben sollte. Wenn während der Arbeit keine Ressource gefunden wird, ist der Fehler aktiviert.

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

    Konstruieren wir die Anfrage an Google, um die notwendige Liste zu erhalten. Geben Sie dazu die Felder aus den Google-Kontaktdaten an, die uns interessieren:

    • names, emailAddresses. Zum Erzeugen der Instanz der Klasse OneContact.
    • memberships. Um zu überprüfen, ob ein Kontakt zu unserer Gruppe gehört.
    • biographies. Dieses Feld ist für die Verwaltung der Kontaktaktivität markiert, obwohl es für die Speicherung der Biografie eines Kontakts vorgesehen ist. Damit ein Kontakt als aktiv erkannt wird und E-Mails an seine Adresse sendet, ist es notwendig, dass das Feld mit eins beginnt. In jedem anderen Fall gilt der Kontakt als passiv und wird ignoriert, auch wenn er sich in der geforderten Gruppe befindet. Es ist nicht notwendig, dieses spezielle Feld dafür zu verwenden. In unserem Fall wird es vermutlich aufgrund seiner relativ seltenen Verwendung ausgewählt. Dies ist für einen Nutzer, der eine Mailing-Kampagne verwaltet, sehr praktisch, da es das "Aktivieren/Deaktivieren" bestimmter Kontakte ermöglicht.

    Zum Schluss stellen wir noch eine Anfrage:

                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)
    

    Stellen Sie eine Anfrage und sortieren Sie die notwendigen Daten im Lambda-Ausdruck. Es sieht ziemlich sperrig aus, ist aber in Wirklichkeit ganz einfach. Ein Kontakt sollte eine Biographie ungleich Null haben, in der richtigen Gruppe liegen, ein aktiver Kontakt sein und eine korrekte Adresse haben. Zeigen wir jetzt die Funktion, die den Status "active/passive" eines einzelnen Kontakts durch den Feldinhalt "Biographien" definiert:

            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)
    

    Dies ist die einzige Funktion im Projekt, die nicht versucht, die Fehler zu reaktivieren, die versuchen, einige von ihnen sofort zu behandeln.

    Wir sind fast fertig! Füge die erhaltene Liste zu _list hinzu. Wenn nicht alle Kontakte gelesen werden, rufen Sie die Funktion rekursiv mit einem neuen Token-Wert auf.

    Senden der E-Mails

    Dies wird durch die folgende Hilfsfunktion durchgeführt:

            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)
    

      Ihr Aufruf ist oben beschrieben. Ziel dieser einfachen Funktion ist es, E-Mails für den Versand vorzubereiten und eine Mailingaktion durchzuführen. Außerdem führt die Funktion über alle "schweren" Vorbereitungsarbeiten aus. Leider akzeptiert Google keine Daten in Form der Klasse MailMessage. Bereiten wir daher die Daten in einer akzeptablen Form auf und kodieren sie. Die Gruppe MimeKit enthält die Werkzeuge, die die Codierung durchführen. Ich glaube jedoch, dass es viel einfacher ist, eine einfache Funktion zu nutzen, die uns zur Verfügung steht. Ich werde es hier nicht zeigen, wegen seiner Einfachheit. Vergessen wir nicht die spezielle userId vom Typ string in dem Aufruf service.Users.Messages.Send. Es entspricht dem speziellen Wert "me", der Google den Zugriff auf Ihr Konto ermöglicht, um die Absenderdaten zu erhalten.

      Damit ist die Analyse der Klasse ContactsPeople abgeschlossen. Die übrigen Funktionen sind Hilfsfunktionen, so dass es keinen Sinn macht, sich mit ihnen zu beschäftigen.

      Die Verbindung zum Terminal

      Das einzige verbleibende Problem ist das Verbinden der (unfertigen) Gruppe mit dem Terminal. Auf den ersten Blick ist die Aufgabe einfach. Wir definieren mehrere 'static' Methoden, kompilieren das Projekt und kopieren es in den Ordner Libraries des Terminals. Rufen wir die 'static' Methoden der Gruppe aus dem MQL-Code auf. Aber was genau sollen wir kopieren? Es gibt eine Gruppe in Form einer DLL-Bibliothek. Es gibt auch etwa ein Dutzend von NuGet heruntergeladene Gruppen, die wir aktiv in unserer Arbeit einsetzen. Es gibt eine JSON-Datei, die Daten für den Zugriff auf Google speichert. Lassen Sie uns versuchen, das gesamte Satz in den Ordner Libraries zu kopieren. Wir erstellen ein primitives MQL-Skript (es macht keinen Sinn, dessen Code hier anzuhängen) und versuchen, eine 'static' Methode aus der Gruppe aufzurufen. Exception! Google.Apis.dll wird nicht gefunden. Dies ist eine sehr unangenehme Überraschung, bedeutet es doch, dass die CLR die gewünschte Gruppe nicht finden kann, obwohl sie sich im gleichen Ordner wie unsere Hauptgruppe befindet. Warum passiert das? Es lohnt sich nicht, die Situation hier im Detail zu betrachten. Alle, die sich für Details interessieren, finden sie in dem berühmten Buch von Richter (im Abschnitt über die Suche nach privaten Sammlungen).

      Es gibt bereits viele Beispiele für voll funktionsfähige .Net-Anwendungen, die mit MetaTrader funktionieren. Solche Probleme traten auch dort auf. Wie wurden sie gelöst? Hier wurde das Problem gelöst, indem ein Kanal zwischen einer .Net-Anwendung und einem MQL-Programm erstellt wurde, während hier ein ereignisbasiertes Modell verwendet wurde. Ich kann einen ähnlichen Ansatz vorschlagen, bei dem die erforderlichen Daten von einem MQL-Programm an eine .Net-Anwendung über die Befehlszeile übergeben werden.

      Aber es lohnt sich, eine "elegantere", einfachere und universellere Lösung in Betracht zu ziehen. Ich meine, die heruntergeladenen Gruppen mit Hilfe des Ereignisses AppDomain.AssemblyResolve zu verwalten. Dieses Ereignis tritt ein , wenn die Ausführungsanforderung eine Gruppe nicht an dem Namen binden kann. In diesem Fall kann die Ereignisbehandlung die Gruppe aus einem anderen Ordner laden und zurückgeben (mit einer Adresse, die die Ereignisbehandlung kennt). Daher bietet sich hier eine recht schöne Lösung an:

      1. Erstellen Sie einen Ordner mit einem anderen Namen im Ordner "Libraries" (in meinem Fall "WorkWithPeople").
      2. Die Gruppe, deren Methoden in eine Datei mit MQL importiert werden sollen, wird in den Ordner "Libraries" kopiert.
      3. Alle anderen Projektgruppen, einschließlich der JSON-Datei mit Daten über den Zugriff auf die Google-Services, werden in den Ordner "WorkWithPeople" kopiert.
      4. Lassen Sie unsere Hauptgruppe im Ordner Libraries wissen, an welcher Adresse sie nach anderen Baugruppen suchen soll — den vollständigen Pfad zum Ordner "WorkWithPeople".

      Als Ergebnis erhalten wir eine praktikable Lösung, ohne den Ordner "Libraries" vollzustopfen. Es bleibt nur noch, die Entscheidungen zu codieren.

      Kontrollklasse

      Lassen Sie uns eine 'static' Klasse erstellen

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

      und fügen wir die Ereignisbehandlung hinzu, so dass er so schnell wie möglich in der Behandlungskette erscheint. Definieren wir die Ereignisbehandlung:

              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)
      

      Nun, wenn eine Gruppe erkannt wird, wird diese Ereignisbehandlung aufgerufen. Ziel ist es, die Gruppe herunterzuladen und zurückzugeben, die den Pfad aus der Variablen _path (definiert während der Initiierung) und den berechneten Namen kombiniert. Nun erscheint der Fehler nur noch, wenn die Ereignisbehandlung die Gruppe nicht finden kann.

      Die Initialisierungsfunktion sieht wie folgt aus:

      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)
      
      

      Diese Funktion sollte als allererste aufgerufen werden, BEVOR ein Versuch zum Versenden von E-Mails unternommen wird. Deren Argumente sind:

      • Path. Der Pfad, wo die Ereignisbehandlung nach Gruppen sucht und wo sich die Datei mit den Daten für den Zugriff auf Google befindet.
      • GoogleGroup. Name einer Gruppe in den Kontakten, die für den Versand verwendet werden.
      • AdminEmail. Kontoname/E-Mail-Adresse (XXX@google.com), die Teilnehmer des Mailings sind.
      • Storage. Ein Name einer Hilfsdatei, in der einige zusätzliche Daten gespeichert sind.

      Alle beschriebenen Argumente sollten keine leeren Zeichenketten sein, da sonst ein Fehler aktiviert wird.

      Erstellen wir eine Liste und eine einfache Funktion zum Hinzufügen, die eingebunden wird:

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

      Die Funktion verfügt über keine Hilfsmittel zur Fehlerprüfung, da es sich um Screenshots und andere Dateien handelt, die zuvor in der MetaTrader-Umgebung erstellt wurden. Es wird davon ausgegangen, dass dies durch das im Terminal arbeitende Programm geschieht.

      Erstellen wir nun ein Objekt für den sofortigen Versand

      static ContactsPeople _cContactsPeople = new ContactsPeople();
      
      

      und führen sie aus, in dem wir sie aufrufen:

      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)
      
      

      Die Argumente sind wie folgt:

      • subject. Betreff der E-Mail.
      • body. Text der E-Mail.
      • isHtml. Ist die E-Mail im Html-Format oder nicht.

      Es gibt zwei Möglichkeiten, _cContactsPeople.WorkWithGoogle aufzurufen, je nachdem, ob eine E-Mail Anlagen enthält. Das erste Argument des Aufrufs ist besonders interessant:

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

      Dies ist ein vollständiger Pfad zur Datei mit Daten für den Zugriff auf Google-Services.

      Die Funktion DoWork(...) gibt die Anzahl der gesendeten E-Mails zurück.

      Das gesamte Projekt für VS++ 2017, mit Ausnahme der Datendatei für den Zugriff auf Google, befindet sich im angehängten Archiv google.zip.

      Vorbereitungen auf der MetaTrader-Seite

      Der Code der Gruppe ist fertig. Kommen wir zum Terminal, um dort ein einfaches Skript zu erstellen. Es kann so geschrieben werden (ein Teil des Codes am Anfang wird übersprungen):

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

      Der Code ist ziemlich offensichtlich. Importieren Sie die Gruppe. Das erste, was wir tun, ist, es zu initialisieren, den zuvor erstellten Screenshot hinzuzufügen und das Mailing durchzuführen. Der vollständige Code befindet sich in der angehängten Datei google_test1.mq5.

      Ein weiteres Beispiel ist ein Indikator, der mit M5 arbeitet und beim Erkennen einer neuen Kerze eine E-Mail mit einem Screenshot sendet:

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

      Der vollständige Code des Indikators befindet sich in der angehängten Datei google_test2.mq5. Es ist sehr einfach, so dass keine weiteren Kommentare erforderlich sind.

      Schlussfolgerung

      Werfen wir einen Blick auf die Ergebnisse. Wir analysierten die Verwendung von Google-Kontakten für die Interaktion mit Partnern sowie die Methode zur Integration von Baugruppen in das Terminal, so dass Benutzer Ordner nicht mit unnötigen Dateien überladen müssen. Erwähnenswert ist auch die Effizienz des Codes der Gruppe. Wir haben uns hier nicht genügend mit diesem Thema beschäftigt, aber es ist möglich, eine Reihe von Aktivitäten anzubieten, um es anzugehen:

      • Trennen Sie die Ziele der Autorisierung in Google und des Versands von E-Mails. Aktivieren Sie die Autorisierung in einem separaten Thread durch einen Timer.
      • Versuchen Sie, einen Thread-Pool für den Versand von E-Mails zu verwenden.
      • Verwenden Sie asynchrone Tools zur "schweren" Kodierung von E-Mail-Anhängen.

      Das bedeutet nicht, dass Sie alle diese Methoden verwenden sollten, aber ihre Verwendung kann die Leistung erhöhen und die Anwendung der resultierenden Gruppe sowohl mit MetaTrader als auch unabhängig voneinander als Teil eines separaten Prozesses ermöglichen.

      Abschließend kommen wir auf die Frage zurück, wie man MQL-Tools zur Lösung dieser Aufgabe einsetzt. Ist es möglich? Laut der Google-Dokumentation ist die Antwort ja. Es ist möglich, die gleichen Ergebnisse mit GET/POST Anfragen zu erzielen, und die entsprechenden Beispiele sind verfügbar. Daher ist es möglich, die reguläre WebRequest zu verwenden. Die Machbarkeit dieser Methode ist nach wie vor eine Frage der Argumentation. Aufgrund einer sehr großen Anzahl von Anfragen wäre es recht schwierig, einen solchen Code zu schreiben, zu debuggen und zu pflegen.

      Programme, die im diesem Artikel verwendet werden

       # Name
      Typ
       Beschreibung
      1 google_test1.mq5
      Skript
      Das Skript, das die Screenshots macht und sie an mehrere Adressen versendet.
      2
      google_test1.mq5 Indikator
      Ein Beispielindikator, der eine E-Mail zu jeder neuen Bar versendet.
      3 google.zip Archive Die Gruppe und das Projekt Testanwendung.

      Übersetzt aus dem Russischen von MetaQuotes Software Corp.
      Originalartikel: https://www.mql5.com/ru/articles/6975

      Beigefügte Dateien |
      google_test1.mq5 (0.95 KB)
      google_test2.mq5 (2.76 KB)
      google.zip (12.71 KB)
      Entwicklung eines plattformübergreifenden Expert Advisors zur Festlegung von StopLoss und TakeProfit basierend auf den Risikoeinstellungen. Entwicklung eines plattformübergreifenden Expert Advisors zur Festlegung von StopLoss und TakeProfit basierend auf den Risikoeinstellungen.

      In diesem Artikel erstellen wir einen Expert Advisor für die automatisierte Berechnung der Losgrößen bei der Eröffnung auf Basis von Risikowerten. Auch der Expert Advisor kann TakeProfit mit dem gewählten Verhältnis zu StopLoss automatisch platzieren. Das heißt, er kann einen TakeProfit basierend auf einem beliebigen Verhältnis berechnen, z.B. 3 zu 1, 4 zu 1 oder einem anderen ausgewählten Wert.

      Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil IX): Kompatibilität mit MQL4 - Datenvorbereitung Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil IX): Kompatibilität mit MQL4 - Datenvorbereitung

      In den vorherigen Artikeln haben wir begonnen, eine große plattformübergreifende Bibliothek zu erstellen, die die Entwicklung von Programmen für MetaTrader 5 und MetaTrader 4 Plattformen vereinfacht. Im achten Teil haben wir die Klasse zur Verfolgung von Ereignissen der Auftrags- und Positionsänderung implementiert. Hier werden wir die Bibliothek verbessern, indem wir die vollständige Kompatibilität mit MQL4 herstellen.

      Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil X): Kompatibilität mit MQL4 - Ereignisse der Positionseröffnung und der Aktivierung von Pending-Orders Bibliothek für ein leichtes und schnelles Entwickeln vom Programmen für den MetaTrader (Teil X): Kompatibilität mit MQL4 - Ereignisse der Positionseröffnung und der Aktivierung von Pending-Orders

      In den vorherigen Artikeln haben wir begonnen, eine große plattformübergreifende Bibliothek zu erstellen, die die Entwicklung von Programmen für MetaTrader 5 und MetaTrader 4 Plattformen vereinfacht. Im neunten Teil haben wir begonnen, die Bibliotheksklassen für die Arbeit mit MQL4 zu verbessern. Hier werden wir die Bibliothek weiter verbessern, um ihre volle Kompatibilität mit MQL4 zu gewährleisten.

      Den Gewinn bis zum letzten Pip extrahieren Den Gewinn bis zum letzten Pip extrahieren

      Der Artikel beschreibt den Versuch, Theorie und Praxis im algorithmischen Handelsbereich zu verbinden. Die meisten Diskussionen über das Erstellen von Handelssystemen stehen im Zusammenhang mit der Verwendung historischer Bars und verschiedener darauf angewandter Indikatoren. Dies ist das am besten abgedeckte Feld und deshalb werden wir es nicht berücksichtigen. Bars sind künstliches Konstrukte, versuchen wir näher an die ursprünglichen Daten zu kommen - den Preis-Ticks.