Budowanie systemu archiwizacji e-maili: Przechowywanie treści e-maila

Ptak

4 mar 2019

Email

1 min read

Budowanie systemu archiwizacji e-maili: Przechowywanie treści e-maila

Najważniejsze informacje

    • Cel: Ten post przedstawia pierwszy etap budowy systemu archiwizacji e-maili przy użyciu SparkPost, Amazon S3 i MySQL. Wyjaśnia, jak duplikować, przechwytywać i przechowywać e-maile w celu długoterminowego dostępu i zgodności.

    • Główna idea: System automatycznie przechowuje surową treść e-maila (format rfc822) w S3 i zapisuje metadane (temat, nadawca, znacznik czasu itp.) w MySQL dla szybkiego wyszukiwania i odzyskiwania.

    • Podstawowe elementy:

      1. Tworzenie duplikatów do archiwizacji: Użyj funkcji Archiwizuj SparkPost, aby wysłać identyczne kopie wychodzących e-maili na wyznaczony adres archiwum, zapewniając, że treść i linki śledzące pozostaną identyczne.

      2. Powiązanie danych za pomocą UID: Osadź unikalny identyfikator (UID) w treści e-maila i metadanych X-MSYS-API, aby powiązać oryginalne i zarchiwizowane wiadomości.

      3. Przetwarzanie przychodzące: Skonfiguruj domenę przychodzącą i webhook w SparkPost, aby odbierać zarchiwizowane ładunki JSON e-maili za pośrednictwem kolektora aplikacji.

      4. Przechowywanie e-maili w S3: Prześlij sparsowaną treść rfc822 do koszyka S3, stosując zasady cyklu życia (np. przejście do Glacier po roku), aby obniżyć koszty przechowywania.

      5. Logowanie metadanych w MySQL: Zapisz kluczowe pola, takie jak RCPT_TO, FROM, SUBJECT i nazwa pliku S3 w celu indeksowania wyszukiwania i przyszłego odzyskiwania.

      6. Rozważania dotyczące wydajności: Efektywność kodu i minimalne logowanie zapewniają, że kolektor może obsługiwać setki żądań na minutę przy minimalnych opóźnieniach.

    • Ogólny obraz: Ta podstawa wspiera przyszłe usprawnienia – takie jak przechowywanie zdarzeń logów, alerty o awariach i wizualizacja interfejsu – kładąc fundamenty pod skalowalne, audytowalne rozwiązanie archiwizacji e-maili.

Podsumowanie pytań i odpowiedzi

  • Jaki jest cel tego projektu?

    Aby stworzyć zautomatyzowany system archiwizacji e-maili, który przechowuje treści wiadomości w Amazon S3, jednocześnie zachowując dane metadane wyszukiwalne w bazie danych MySQL.

  • Dlaczego warto używać funkcji Archiwum SparkPost?

    Umożliwia generowanie dokładnych duplikatów wychodzących e-maili, zachowując ich strukturę oraz dane śledzenia dla celów zgodności i przeglądu.

  • Jak każdy zarchiwizowany email jest powiązany ze swoją oryginalną wiadomością?

    Unikalny identyfikator UID jest osadzony zarówno w treści wiadomości e-mail, jak i w metadanych, umożliwiając dokładne odniesienie między oryginalną a zarchiwizowaną kopią.

  • Dlaczego używać S3 do przechowywania?

    S3 oferuje skalowalny magazyn i opcje zarządzania cyklem życia (jak Glacier), co czyni go opłacalnym dla długoterminowego przechowywania e-maili.

  • Co przechowuje baza danych MySQL?

    Przechowuje pola metadanych, które można przeszukiwać—takie jak linia tematu, nadawca, znaczniki czasu i nazwa pliku S3—umożliwiając wydajne zapytania i pobieranie.

  • Jakie są następne kroki rozwoju?

    Dodawanie śledzenia zdarzeń logów, zautomatyzowanego raportowania błędów, uproszczonego kolektora oraz interfejsu do przeglądania lub ponownego wysyłania zarchiwizowanych e-maili.

W tym blogu opiszę proces, przez który przeszedłem, aby przechować treść e-maila w S3 (Amazon’s Simple Store Service) oraz dane dodatkowe w tabeli MySQL dla łatwego krzyżowego odniesienia. Ostatecznie jest to punkt wyjścia dla bazy kodu, która zawiera aplikację, która umożliwi łatwe przeszukiwanie archiwalnych e-maili, a następnie wyświetlanie tych e-maili wraz z danymi o wydarzeniach (log). Kod do tego projektu znajduje się w następującym repozytorium GitHub: https://github.com/jeff-goldstein/PHPArchivePlatform.

Chociaż w tym projekcie wykorzystam S3 i MySQL, w żadnym wypadku nie są to jedyne technologie, które można wykorzystać do budowy platformy archiwizacyjnej, ale biorąc pod uwagę ich wszechobecność, pomyślałem, że są dobrym wyborem do tego projektu. W systemie o pełnej skali o wysokiej wydajności użyłbym bazy danych o wyższej wydajności niż MySQL, ale dla tego przykładowego projektu MySQL jest idealne. Dla organizacji rozważających PostgreSQL jako wybór bazy danych do archiwizacji, wdrożenie odpowiednich procedur tworzenia kopii zapasowych i przywracania jest niezbędne do utrzymania integralności danych w systemach produkcyjnych.

Szczegółowo opisałem poniżej kroki, które podjąłem w tej pierwszej fazie projektu:

  1. Tworzenie duplikatu e-maila do archiwizacji

  2. Wykorzystanie funkcji archiwizacji i przekazywania przychodzącego SparkPost do wysłania kopii oryginalnego e-maila z powrotem do SparkPost w celu przetworzenia na strukturę JSON, a następnie wysłania do zbieracza webhook (aplikacja)

  3. Rozmontowanie struktury JSON w celu uzyskania niezbędnych komponentów

  4. Wysłanie treści e-maila do S3 w celu przechowywania

  5. Zalogowanie wpisu do MySQL dla każdego e-maila w celu krzyżowego odniesienia

W tym blogu opiszę proces, przez który przeszedłem, aby przechować treść e-maila w S3 (Amazon’s Simple Store Service) oraz dane dodatkowe w tabeli MySQL dla łatwego krzyżowego odniesienia. Ostatecznie jest to punkt wyjścia dla bazy kodu, która zawiera aplikację, która umożliwi łatwe przeszukiwanie archiwalnych e-maili, a następnie wyświetlanie tych e-maili wraz z danymi o wydarzeniach (log). Kod do tego projektu znajduje się w następującym repozytorium GitHub: https://github.com/jeff-goldstein/PHPArchivePlatform.

Chociaż w tym projekcie wykorzystam S3 i MySQL, w żadnym wypadku nie są to jedyne technologie, które można wykorzystać do budowy platformy archiwizacyjnej, ale biorąc pod uwagę ich wszechobecność, pomyślałem, że są dobrym wyborem do tego projektu. W systemie o pełnej skali o wysokiej wydajności użyłbym bazy danych o wyższej wydajności niż MySQL, ale dla tego przykładowego projektu MySQL jest idealne. Dla organizacji rozważających PostgreSQL jako wybór bazy danych do archiwizacji, wdrożenie odpowiednich procedur tworzenia kopii zapasowych i przywracania jest niezbędne do utrzymania integralności danych w systemach produkcyjnych.

Szczegółowo opisałem poniżej kroki, które podjąłem w tej pierwszej fazie projektu:

  1. Tworzenie duplikatu e-maila do archiwizacji

  2. Wykorzystanie funkcji archiwizacji i przekazywania przychodzącego SparkPost do wysłania kopii oryginalnego e-maila z powrotem do SparkPost w celu przetworzenia na strukturę JSON, a następnie wysłania do zbieracza webhook (aplikacja)

  3. Rozmontowanie struktury JSON w celu uzyskania niezbędnych komponentów

  4. Wysłanie treści e-maila do S3 w celu przechowywania

  5. Zalogowanie wpisu do MySQL dla każdego e-maila w celu krzyżowego odniesienia

W tym blogu opiszę proces, przez który przeszedłem, aby przechować treść e-maila w S3 (Amazon’s Simple Store Service) oraz dane dodatkowe w tabeli MySQL dla łatwego krzyżowego odniesienia. Ostatecznie jest to punkt wyjścia dla bazy kodu, która zawiera aplikację, która umożliwi łatwe przeszukiwanie archiwalnych e-maili, a następnie wyświetlanie tych e-maili wraz z danymi o wydarzeniach (log). Kod do tego projektu znajduje się w następującym repozytorium GitHub: https://github.com/jeff-goldstein/PHPArchivePlatform.

Chociaż w tym projekcie wykorzystam S3 i MySQL, w żadnym wypadku nie są to jedyne technologie, które można wykorzystać do budowy platformy archiwizacyjnej, ale biorąc pod uwagę ich wszechobecność, pomyślałem, że są dobrym wyborem do tego projektu. W systemie o pełnej skali o wysokiej wydajności użyłbym bazy danych o wyższej wydajności niż MySQL, ale dla tego przykładowego projektu MySQL jest idealne. Dla organizacji rozważających PostgreSQL jako wybór bazy danych do archiwizacji, wdrożenie odpowiednich procedur tworzenia kopii zapasowych i przywracania jest niezbędne do utrzymania integralności danych w systemach produkcyjnych.

Szczegółowo opisałem poniżej kroki, które podjąłem w tej pierwszej fazie projektu:

  1. Tworzenie duplikatu e-maila do archiwizacji

  2. Wykorzystanie funkcji archiwizacji i przekazywania przychodzącego SparkPost do wysłania kopii oryginalnego e-maila z powrotem do SparkPost w celu przetworzenia na strukturę JSON, a następnie wysłania do zbieracza webhook (aplikacja)

  3. Rozmontowanie struktury JSON w celu uzyskania niezbędnych komponentów

  4. Wysłanie treści e-maila do S3 w celu przechowywania

  5. Zalogowanie wpisu do MySQL dla każdego e-maila w celu krzyżowego odniesienia

Tworzenie duplikatu wiadomości e-mail

W SparkPost najlepszym sposobem archiwizacji wiadomości e-mail jest stworzenie identycznej kopii wiadomości e-mail, specjalnie zaprojektowanej do celów archiwalnych. Dokonuje się tego za pomocą funkcji Archiwum SparkPost. Funkcja Archiwum SparkPost umożliwia nadawcy wysłanie duplikatu wiadomości e-mail na jeden lub więcej adresów e-mail.  Ten duplikat używa tych samych linków śledzenia i otwierania, co oryginał. Dokumentacja SparkPost definiuje funkcję Archiwum w następujący sposób:

Odbiorcy na liście archiwum otrzymają dokładną replikę wiadomości, która została wysłana na adres RCPT TO. W szczególności wszelkie zakodowane linki przeznaczone dla odbiorcy RCPT TO będą identyczne w wiadomościach archiwum.

Jedyną różnicą między tą kopią archiwalną a oryginalnym e-mailem RCPT TO jest to, że niektóre nagłówki będą różne, ponieważ docelowy adres dla archiwizowanej wiadomości e-mail jest inny, ale treść wiadomości e-mail będzie dokładną repliką!

Jeśli chcesz głębszego wyjaśnienia, oto link do dokumentacji SparkPost na temat tworzenia duplikatów (lub archiwalnych) kopii wiadomości e-mail. Przykładowe nagłówki X-MSYS-API dla tego projektu zostaną przedstawione później w tym blogu.

Jest jeden warunek dotyczący tego podejścia; podczas gdy wszystkie informacje o zdarzeniu w oryginalnym e-mailu są powiązane zarówno przez transmission_id, jak i message_id, nie ma informacji w zdarzeniu przychodzącym (mechanizm pozyskiwania i dystrybucji wiadomości archiwalnej) dla duplikatu e-maila, które odnosi się do jednego z tych dwóch identyfikatorów i tym samym informacji o oryginalnym e-mailu. Oznacza to, że musimy umieścić dane w treści wiadomości e-mail oraz nagłówku oryginalnego e-maila jako sposób na powiązanie wszystkich danych SparkPost z oryginalnym e-mailem i e-mailem archiwalnym.

Aby stworzyć kod, który zostanie umieszczony w treści wiadomości e-mail, użyłem następującego procesu w aplikacji do tworzenia wiadomości e-mail.

  1. Gdzieś w treści wiadomości e-mail umieściłem następujący wpis:<input name="ArchiveCode" type="hidden" value="<<UID>>">

  2. Następnie stworzyłem unikalny kod i zastąpiłem pole <<UID>>:$uid = md5(uniqid(rand(), true)); $emailBody = str_replace(“<<UID>>,$uid,$emailBody);

    Oto przykład wyjściowy:

    <input name="ArchiveCode" type="hidden" value="00006365263145">

  3. Następnie upewniłem się, że dodałem $UID do bloku meta_data nagłówka X-MSYS-API. Ten krok zapewnia, że UID jest wbudowany w każdy wynik zdarzenia dla oryginalnego e-maila:

X-MSYS-API: {
  "campaign_id": "<my_campaign>",
  "metadata": {
    "UID": "<UID>"
  },
  "archive": [
    {
      "email": "archive@geekwithapersonality.com"
    }
  ],
  "options": {
    "open_tracking": false,
    "click_tracking": false,
    "transactional": false,
    "ip_pool": "<my_ip_pool>"
  }
}

Teraz mamy sposób na powiązanie wszystkich danych z oryginalnego e-maila z treścią archiwum.

W SparkPost najlepszym sposobem archiwizacji wiadomości e-mail jest stworzenie identycznej kopii wiadomości e-mail, specjalnie zaprojektowanej do celów archiwalnych. Dokonuje się tego za pomocą funkcji Archiwum SparkPost. Funkcja Archiwum SparkPost umożliwia nadawcy wysłanie duplikatu wiadomości e-mail na jeden lub więcej adresów e-mail.  Ten duplikat używa tych samych linków śledzenia i otwierania, co oryginał. Dokumentacja SparkPost definiuje funkcję Archiwum w następujący sposób:

Odbiorcy na liście archiwum otrzymają dokładną replikę wiadomości, która została wysłana na adres RCPT TO. W szczególności wszelkie zakodowane linki przeznaczone dla odbiorcy RCPT TO będą identyczne w wiadomościach archiwum.

Jedyną różnicą między tą kopią archiwalną a oryginalnym e-mailem RCPT TO jest to, że niektóre nagłówki będą różne, ponieważ docelowy adres dla archiwizowanej wiadomości e-mail jest inny, ale treść wiadomości e-mail będzie dokładną repliką!

Jeśli chcesz głębszego wyjaśnienia, oto link do dokumentacji SparkPost na temat tworzenia duplikatów (lub archiwalnych) kopii wiadomości e-mail. Przykładowe nagłówki X-MSYS-API dla tego projektu zostaną przedstawione później w tym blogu.

Jest jeden warunek dotyczący tego podejścia; podczas gdy wszystkie informacje o zdarzeniu w oryginalnym e-mailu są powiązane zarówno przez transmission_id, jak i message_id, nie ma informacji w zdarzeniu przychodzącym (mechanizm pozyskiwania i dystrybucji wiadomości archiwalnej) dla duplikatu e-maila, które odnosi się do jednego z tych dwóch identyfikatorów i tym samym informacji o oryginalnym e-mailu. Oznacza to, że musimy umieścić dane w treści wiadomości e-mail oraz nagłówku oryginalnego e-maila jako sposób na powiązanie wszystkich danych SparkPost z oryginalnym e-mailem i e-mailem archiwalnym.

Aby stworzyć kod, który zostanie umieszczony w treści wiadomości e-mail, użyłem następującego procesu w aplikacji do tworzenia wiadomości e-mail.

  1. Gdzieś w treści wiadomości e-mail umieściłem następujący wpis:<input name="ArchiveCode" type="hidden" value="<<UID>>">

  2. Następnie stworzyłem unikalny kod i zastąpiłem pole <<UID>>:$uid = md5(uniqid(rand(), true)); $emailBody = str_replace(“<<UID>>,$uid,$emailBody);

    Oto przykład wyjściowy:

    <input name="ArchiveCode" type="hidden" value="00006365263145">

  3. Następnie upewniłem się, że dodałem $UID do bloku meta_data nagłówka X-MSYS-API. Ten krok zapewnia, że UID jest wbudowany w każdy wynik zdarzenia dla oryginalnego e-maila:

X-MSYS-API: {
  "campaign_id": "<my_campaign>",
  "metadata": {
    "UID": "<UID>"
  },
  "archive": [
    {
      "email": "archive@geekwithapersonality.com"
    }
  ],
  "options": {
    "open_tracking": false,
    "click_tracking": false,
    "transactional": false,
    "ip_pool": "<my_ip_pool>"
  }
}

Teraz mamy sposób na powiązanie wszystkich danych z oryginalnego e-maila z treścią archiwum.

W SparkPost najlepszym sposobem archiwizacji wiadomości e-mail jest stworzenie identycznej kopii wiadomości e-mail, specjalnie zaprojektowanej do celów archiwalnych. Dokonuje się tego za pomocą funkcji Archiwum SparkPost. Funkcja Archiwum SparkPost umożliwia nadawcy wysłanie duplikatu wiadomości e-mail na jeden lub więcej adresów e-mail.  Ten duplikat używa tych samych linków śledzenia i otwierania, co oryginał. Dokumentacja SparkPost definiuje funkcję Archiwum w następujący sposób:

Odbiorcy na liście archiwum otrzymają dokładną replikę wiadomości, która została wysłana na adres RCPT TO. W szczególności wszelkie zakodowane linki przeznaczone dla odbiorcy RCPT TO będą identyczne w wiadomościach archiwum.

Jedyną różnicą między tą kopią archiwalną a oryginalnym e-mailem RCPT TO jest to, że niektóre nagłówki będą różne, ponieważ docelowy adres dla archiwizowanej wiadomości e-mail jest inny, ale treść wiadomości e-mail będzie dokładną repliką!

Jeśli chcesz głębszego wyjaśnienia, oto link do dokumentacji SparkPost na temat tworzenia duplikatów (lub archiwalnych) kopii wiadomości e-mail. Przykładowe nagłówki X-MSYS-API dla tego projektu zostaną przedstawione później w tym blogu.

Jest jeden warunek dotyczący tego podejścia; podczas gdy wszystkie informacje o zdarzeniu w oryginalnym e-mailu są powiązane zarówno przez transmission_id, jak i message_id, nie ma informacji w zdarzeniu przychodzącym (mechanizm pozyskiwania i dystrybucji wiadomości archiwalnej) dla duplikatu e-maila, które odnosi się do jednego z tych dwóch identyfikatorów i tym samym informacji o oryginalnym e-mailu. Oznacza to, że musimy umieścić dane w treści wiadomości e-mail oraz nagłówku oryginalnego e-maila jako sposób na powiązanie wszystkich danych SparkPost z oryginalnym e-mailem i e-mailem archiwalnym.

Aby stworzyć kod, który zostanie umieszczony w treści wiadomości e-mail, użyłem następującego procesu w aplikacji do tworzenia wiadomości e-mail.

  1. Gdzieś w treści wiadomości e-mail umieściłem następujący wpis:<input name="ArchiveCode" type="hidden" value="<<UID>>">

  2. Następnie stworzyłem unikalny kod i zastąpiłem pole <<UID>>:$uid = md5(uniqid(rand(), true)); $emailBody = str_replace(“<<UID>>,$uid,$emailBody);

    Oto przykład wyjściowy:

    <input name="ArchiveCode" type="hidden" value="00006365263145">

  3. Następnie upewniłem się, że dodałem $UID do bloku meta_data nagłówka X-MSYS-API. Ten krok zapewnia, że UID jest wbudowany w każdy wynik zdarzenia dla oryginalnego e-maila:

X-MSYS-API: {
  "campaign_id": "<my_campaign>",
  "metadata": {
    "UID": "<UID>"
  },
  "archive": [
    {
      "email": "archive@geekwithapersonality.com"
    }
  ],
  "options": {
    "open_tracking": false,
    "click_tracking": false,
    "transactional": false,
    "ip_pool": "<my_ip_pool>"
  }
}

Teraz mamy sposób na powiązanie wszystkich danych z oryginalnego e-maila z treścią archiwum.

Uzyskiwanie wersji archiwalnej

Aby uzyskać kopię e-maila do archiwum, należy wykonać następujące kroki:

  1. Utwórz subdomenę, na którą będziesz wysyłać wszystkie archiwalne (duplikatowe) e-maile

  2. Ustaw odpowiednie rekordy DNS, aby wszystkie e-maile wysyłane na tę subdomenę były kierowane do SparkPost

  3. Utwórz domenę przychodzącą w SparkPost

  4. Utwórz webhook przychodzący w SparkPost

  5. Utwórz aplikację (kolektor), aby odbierać strumień danych webhooka SparkPost

Poniższe dwa linki mogą być użyteczne, aby poprowadzić Cię przez ten proces:

  1. Dokumentacja techniczna SparkPost: Włączanie przesyłania e-maili przychodzących i webhooków

  2. Również blog, który napisałem w zeszłym roku, Archiwizowanie e-maili: Przewodnik, jak śledzić wysłane wiadomości przeprowadzi Cię przez tworzenie relayu przychodzącego w SparkPost

* Uwaga: od października 2018 roku funkcja archiwum działa tylko w przypadku wysyłania e-maili przy użyciu połączenia SMTP z SparkPost, interfejs API RESTful nie obsługuje tej funkcji.  To prawdopodobnie nie jest problem, ponieważ większość e-maili, które wymagają tego poziomu kontroli audytu, ma tendencję do bycia spersonalizowanymi e-mailami, które są w pełni przygotowane przez aplikację zaplecza przed potrzebą dostarczenia e-maila.

Aby uzyskać kopię e-maila do archiwum, należy wykonać następujące kroki:

  1. Utwórz subdomenę, na którą będziesz wysyłać wszystkie archiwalne (duplikatowe) e-maile

  2. Ustaw odpowiednie rekordy DNS, aby wszystkie e-maile wysyłane na tę subdomenę były kierowane do SparkPost

  3. Utwórz domenę przychodzącą w SparkPost

  4. Utwórz webhook przychodzący w SparkPost

  5. Utwórz aplikację (kolektor), aby odbierać strumień danych webhooka SparkPost

Poniższe dwa linki mogą być użyteczne, aby poprowadzić Cię przez ten proces:

  1. Dokumentacja techniczna SparkPost: Włączanie przesyłania e-maili przychodzących i webhooków

  2. Również blog, który napisałem w zeszłym roku, Archiwizowanie e-maili: Przewodnik, jak śledzić wysłane wiadomości przeprowadzi Cię przez tworzenie relayu przychodzącego w SparkPost

* Uwaga: od października 2018 roku funkcja archiwum działa tylko w przypadku wysyłania e-maili przy użyciu połączenia SMTP z SparkPost, interfejs API RESTful nie obsługuje tej funkcji.  To prawdopodobnie nie jest problem, ponieważ większość e-maili, które wymagają tego poziomu kontroli audytu, ma tendencję do bycia spersonalizowanymi e-mailami, które są w pełni przygotowane przez aplikację zaplecza przed potrzebą dostarczenia e-maila.

Aby uzyskać kopię e-maila do archiwum, należy wykonać następujące kroki:

  1. Utwórz subdomenę, na którą będziesz wysyłać wszystkie archiwalne (duplikatowe) e-maile

  2. Ustaw odpowiednie rekordy DNS, aby wszystkie e-maile wysyłane na tę subdomenę były kierowane do SparkPost

  3. Utwórz domenę przychodzącą w SparkPost

  4. Utwórz webhook przychodzący w SparkPost

  5. Utwórz aplikację (kolektor), aby odbierać strumień danych webhooka SparkPost

Poniższe dwa linki mogą być użyteczne, aby poprowadzić Cię przez ten proces:

  1. Dokumentacja techniczna SparkPost: Włączanie przesyłania e-maili przychodzących i webhooków

  2. Również blog, który napisałem w zeszłym roku, Archiwizowanie e-maili: Przewodnik, jak śledzić wysłane wiadomości przeprowadzi Cię przez tworzenie relayu przychodzącego w SparkPost

* Uwaga: od października 2018 roku funkcja archiwum działa tylko w przypadku wysyłania e-maili przy użyciu połączenia SMTP z SparkPost, interfejs API RESTful nie obsługuje tej funkcji.  To prawdopodobnie nie jest problem, ponieważ większość e-maili, które wymagają tego poziomu kontroli audytu, ma tendencję do bycia spersonalizowanymi e-mailami, które są w pełni przygotowane przez aplikację zaplecza przed potrzebą dostarczenia e-maila.

Uzyskanie duplikatu adresu e-mail w strukturze JSON

W pierwszej fazie tego projektu przechowuję tylko format e-maila rfc822 w S3 oraz kilka ogólnych pól opisowych w tabeli SQL do przeszukiwania.  Ponieważ SparkPost będzie przesyłać dane e-mail w strukturze JSON do mojej platformy archiwizacyjnej za pośrednictwem strumieni danych webhook, zbudowałem aplikację (często określaną jako collector), która akceptuje strumień danych Relay_Webhook.

Każda paczka z Relay_Webhook SparkPost będzie zawierała informacje o jednym duplikacie e-maila w danym czasie, więc rozbicie struktury JSON na docelowe komponenty dla tego projektu jest dość proste.  W moim kodzie PHP, uzyskanie e-maila sformatowanego jako rfc822 było tak łatwe, jak poniższe kilka linii kodu:

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $body = file_get_contents("php://input");
    $fields = json_decode($body, true);
    $rfc822body = $fields[0]['msys']['relay_message']['content']['email_rfc822'];
    $htmlbody = $fields[0]['msys']['relay_message']['content']['html'];
    $headers  = $fields[0]['msys']['relay_message']['content']['headers'];
}

Niektóre z informacji, które chcę przechować w mojej tabeli SQL, znajdują się w tablicy pól nagłówków.  Dlatego napisałem małą funkcję, która akceptowała tablicę nagłówków i przechodziła przez tablicę, aby uzyskać dane, którymi byłem zainteresowany:

function get_important_headers($headers, &$original_to, &$headerDate, &$subject, &$from) {
    foreach ($headers as $headerGroup) {
        foreach ($headerGroup as $key => $value) {
            if ($key === 'To') {
                $original_to = $value;
            } elseif ($key === 'Date') {
                $headerDate = $value;
            } elseif ($key === 'Subject') {
                $subject = $value;
            } elseif ($key === 'From') {
                $from = $value;
            }
        }
    }
}

Teraz, gdy mam już dane, jestem gotowy, aby zapisać treść do S3.

W pierwszej fazie tego projektu przechowuję tylko format e-maila rfc822 w S3 oraz kilka ogólnych pól opisowych w tabeli SQL do przeszukiwania.  Ponieważ SparkPost będzie przesyłać dane e-mail w strukturze JSON do mojej platformy archiwizacyjnej za pośrednictwem strumieni danych webhook, zbudowałem aplikację (często określaną jako collector), która akceptuje strumień danych Relay_Webhook.

Każda paczka z Relay_Webhook SparkPost będzie zawierała informacje o jednym duplikacie e-maila w danym czasie, więc rozbicie struktury JSON na docelowe komponenty dla tego projektu jest dość proste.  W moim kodzie PHP, uzyskanie e-maila sformatowanego jako rfc822 było tak łatwe, jak poniższe kilka linii kodu:

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $body = file_get_contents("php://input");
    $fields = json_decode($body, true);
    $rfc822body = $fields[0]['msys']['relay_message']['content']['email_rfc822'];
    $htmlbody = $fields[0]['msys']['relay_message']['content']['html'];
    $headers  = $fields[0]['msys']['relay_message']['content']['headers'];
}

Niektóre z informacji, które chcę przechować w mojej tabeli SQL, znajdują się w tablicy pól nagłówków.  Dlatego napisałem małą funkcję, która akceptowała tablicę nagłówków i przechodziła przez tablicę, aby uzyskać dane, którymi byłem zainteresowany:

function get_important_headers($headers, &$original_to, &$headerDate, &$subject, &$from) {
    foreach ($headers as $headerGroup) {
        foreach ($headerGroup as $key => $value) {
            if ($key === 'To') {
                $original_to = $value;
            } elseif ($key === 'Date') {
                $headerDate = $value;
            } elseif ($key === 'Subject') {
                $subject = $value;
            } elseif ($key === 'From') {
                $from = $value;
            }
        }
    }
}

Teraz, gdy mam już dane, jestem gotowy, aby zapisać treść do S3.

W pierwszej fazie tego projektu przechowuję tylko format e-maila rfc822 w S3 oraz kilka ogólnych pól opisowych w tabeli SQL do przeszukiwania.  Ponieważ SparkPost będzie przesyłać dane e-mail w strukturze JSON do mojej platformy archiwizacyjnej za pośrednictwem strumieni danych webhook, zbudowałem aplikację (często określaną jako collector), która akceptuje strumień danych Relay_Webhook.

Każda paczka z Relay_Webhook SparkPost będzie zawierała informacje o jednym duplikacie e-maila w danym czasie, więc rozbicie struktury JSON na docelowe komponenty dla tego projektu jest dość proste.  W moim kodzie PHP, uzyskanie e-maila sformatowanego jako rfc822 było tak łatwe, jak poniższe kilka linii kodu:

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $body = file_get_contents("php://input");
    $fields = json_decode($body, true);
    $rfc822body = $fields[0]['msys']['relay_message']['content']['email_rfc822'];
    $htmlbody = $fields[0]['msys']['relay_message']['content']['html'];
    $headers  = $fields[0]['msys']['relay_message']['content']['headers'];
}

Niektóre z informacji, które chcę przechować w mojej tabeli SQL, znajdują się w tablicy pól nagłówków.  Dlatego napisałem małą funkcję, która akceptowała tablicę nagłówków i przechodziła przez tablicę, aby uzyskać dane, którymi byłem zainteresowany:

function get_important_headers($headers, &$original_to, &$headerDate, &$subject, &$from) {
    foreach ($headers as $headerGroup) {
        foreach ($headerGroup as $key => $value) {
            if ($key === 'To') {
                $original_to = $value;
            } elseif ($key === 'Date') {
                $headerDate = $value;
            } elseif ($key === 'Subject') {
                $subject = $value;
            } elseif ($key === 'From') {
                $from = $value;
            }
        }
    }
}

Teraz, gdy mam już dane, jestem gotowy, aby zapisać treść do S3.

Przechowywanie duplikatu adresu e-mail w S3

Przykro mi Cię rozczarować, ale nie zamierzam dawać szczegółowego samouczka na temat tworzenia wiadra S3 do przechowywania e-maili, ani nie zamierzam opisywać, jak stworzyć niezbędny klucz dostępu, który będziesz potrzebować w swojej aplikacji do przesyłania treści do swojego wiadra; są lepsze samouczki na ten temat, niż kiedykolwiek mógłbym napisać.  Oto kilka artykułów, które mogą pomóc:

https://docs.aws.amazon.com/quickstarts/latest/s3backup/step-1-create-bucket.html
https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/

To, co zrobię, to zwrócenie uwagi na niektóre z ustawień, które wybrałem, dotyczących takiego projektu.

  1. Kontrola dostępu.  Nie tylko musisz ustawić zabezpieczenia dla wiadra, ale także musisz ustawić uprawnienia dla samych elementów.  W moim projekcie stosuję bardzo otwartą politykę public-read, ponieważ przykładowe dane nie są osobiste, a chciałem mieć łatwy dostęp do danych.  Prawdopodobnie będziesz chciał znacznie restrykcyjniejszy zestaw polityk ACL. Oto ładny artykuł na temat ustawień ACL: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html

  2. Archiwizowanie archiwum. W S3 jest coś, co nazywa się zarządzaniem cyklem życia.  Dzięki temu możesz przenieść dane z jednej klasy przechowywania S3 do innej.  Różne klasy przechowywania reprezentują stopień dostępu, jaki potrzebujesz do przechowywanych danych, przy niższych kosztach związanych z przechowaniem, do którego masz najmniejszy dostęp. Dobry opis różnych klas i przechodzenia przez nie można znaleźć w przewodniku AWS zatytułowanym Transitioning Objects. W moim przypadku zdecydowałem się stworzyć cykl życia, który przenosi każdy obiekt z Standard do Glacier po roku. Dostęp do Glacier jest znacznie tańszy niż standardowe archiwum S3 i pozwoli mi zaoszczędzić na kosztach przechowywania.

Gdy już stworzyłem wiadro S3 i ustawiłem moje parametry, S3 jest gotowe, abym mógł przesłać e-mail zgodny z rfc822, który uzyskałem z strumienia danych webhooka SparkPost Relay. Ale przed przesłaniem ładunku e-maila rfc822 do S3 muszę stworzyć unikalną nazwę pliku, którą użyję do przechowywania tego e-maila.

Dla unikalnej nazwy pliku zamierzam przeszukać treść e-maila w poszukiwaniu ukrytego id, które aplikacja wysyłająca umieściła w e-mailu i użyć tego id jako nazwy pliku. Istnieją bardziej eleganckie sposoby na pobranie connectorId z treści html, ale dla prostoty i jasności zamierzam użyć następującego kodu:

$start = strpos($htmlbody, $inputField);
if ($start !== false) {
    $start = strpos($htmlbody, 'value="', $start);
    if ($start !== false) {
        $start += 7; // Move past 'value="'
        $end = strpos($htmlbody, '"', $start);
        if ($end !== false) {
            $length = $end - $start;
            $UID = substr($htmlbody, $start, $length);
        }
    }
}

* zakładamy, że $inputField zawiera wartość "ArchiveCode" i została znaleziona w moim pliku config.php.

Z UID możemy następnie utworzyć nazwę pliku, która będzie używana w S3:

$fileName = $ArchiveDirectory . '/' . $UID . '.eml';

Teraz mogę otworzyć moje połączenie z S3 i przesłać plik. Jeśli spojrzysz na plik s3.php w repozytorium GitHub, zobaczysz, że potrzeba bardzo mało kodu, aby przesłać plik.

Moim ostatnim krokiem jest zarejestrowanie tego wpisu w tabeli MYSQL.

Przykro mi Cię rozczarować, ale nie zamierzam dawać szczegółowego samouczka na temat tworzenia wiadra S3 do przechowywania e-maili, ani nie zamierzam opisywać, jak stworzyć niezbędny klucz dostępu, który będziesz potrzebować w swojej aplikacji do przesyłania treści do swojego wiadra; są lepsze samouczki na ten temat, niż kiedykolwiek mógłbym napisać.  Oto kilka artykułów, które mogą pomóc:

https://docs.aws.amazon.com/quickstarts/latest/s3backup/step-1-create-bucket.html
https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/

To, co zrobię, to zwrócenie uwagi na niektóre z ustawień, które wybrałem, dotyczących takiego projektu.

  1. Kontrola dostępu.  Nie tylko musisz ustawić zabezpieczenia dla wiadra, ale także musisz ustawić uprawnienia dla samych elementów.  W moim projekcie stosuję bardzo otwartą politykę public-read, ponieważ przykładowe dane nie są osobiste, a chciałem mieć łatwy dostęp do danych.  Prawdopodobnie będziesz chciał znacznie restrykcyjniejszy zestaw polityk ACL. Oto ładny artykuł na temat ustawień ACL: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html

  2. Archiwizowanie archiwum. W S3 jest coś, co nazywa się zarządzaniem cyklem życia.  Dzięki temu możesz przenieść dane z jednej klasy przechowywania S3 do innej.  Różne klasy przechowywania reprezentują stopień dostępu, jaki potrzebujesz do przechowywanych danych, przy niższych kosztach związanych z przechowaniem, do którego masz najmniejszy dostęp. Dobry opis różnych klas i przechodzenia przez nie można znaleźć w przewodniku AWS zatytułowanym Transitioning Objects. W moim przypadku zdecydowałem się stworzyć cykl życia, który przenosi każdy obiekt z Standard do Glacier po roku. Dostęp do Glacier jest znacznie tańszy niż standardowe archiwum S3 i pozwoli mi zaoszczędzić na kosztach przechowywania.

Gdy już stworzyłem wiadro S3 i ustawiłem moje parametry, S3 jest gotowe, abym mógł przesłać e-mail zgodny z rfc822, który uzyskałem z strumienia danych webhooka SparkPost Relay. Ale przed przesłaniem ładunku e-maila rfc822 do S3 muszę stworzyć unikalną nazwę pliku, którą użyję do przechowywania tego e-maila.

Dla unikalnej nazwy pliku zamierzam przeszukać treść e-maila w poszukiwaniu ukrytego id, które aplikacja wysyłająca umieściła w e-mailu i użyć tego id jako nazwy pliku. Istnieją bardziej eleganckie sposoby na pobranie connectorId z treści html, ale dla prostoty i jasności zamierzam użyć następującego kodu:

$start = strpos($htmlbody, $inputField);
if ($start !== false) {
    $start = strpos($htmlbody, 'value="', $start);
    if ($start !== false) {
        $start += 7; // Move past 'value="'
        $end = strpos($htmlbody, '"', $start);
        if ($end !== false) {
            $length = $end - $start;
            $UID = substr($htmlbody, $start, $length);
        }
    }
}

* zakładamy, że $inputField zawiera wartość "ArchiveCode" i została znaleziona w moim pliku config.php.

Z UID możemy następnie utworzyć nazwę pliku, która będzie używana w S3:

$fileName = $ArchiveDirectory . '/' . $UID . '.eml';

Teraz mogę otworzyć moje połączenie z S3 i przesłać plik. Jeśli spojrzysz na plik s3.php w repozytorium GitHub, zobaczysz, że potrzeba bardzo mało kodu, aby przesłać plik.

Moim ostatnim krokiem jest zarejestrowanie tego wpisu w tabeli MYSQL.

Przykro mi Cię rozczarować, ale nie zamierzam dawać szczegółowego samouczka na temat tworzenia wiadra S3 do przechowywania e-maili, ani nie zamierzam opisywać, jak stworzyć niezbędny klucz dostępu, który będziesz potrzebować w swojej aplikacji do przesyłania treści do swojego wiadra; są lepsze samouczki na ten temat, niż kiedykolwiek mógłbym napisać.  Oto kilka artykułów, które mogą pomóc:

https://docs.aws.amazon.com/quickstarts/latest/s3backup/step-1-create-bucket.html
https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/

To, co zrobię, to zwrócenie uwagi na niektóre z ustawień, które wybrałem, dotyczących takiego projektu.

  1. Kontrola dostępu.  Nie tylko musisz ustawić zabezpieczenia dla wiadra, ale także musisz ustawić uprawnienia dla samych elementów.  W moim projekcie stosuję bardzo otwartą politykę public-read, ponieważ przykładowe dane nie są osobiste, a chciałem mieć łatwy dostęp do danych.  Prawdopodobnie będziesz chciał znacznie restrykcyjniejszy zestaw polityk ACL. Oto ładny artykuł na temat ustawień ACL: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html

  2. Archiwizowanie archiwum. W S3 jest coś, co nazywa się zarządzaniem cyklem życia.  Dzięki temu możesz przenieść dane z jednej klasy przechowywania S3 do innej.  Różne klasy przechowywania reprezentują stopień dostępu, jaki potrzebujesz do przechowywanych danych, przy niższych kosztach związanych z przechowaniem, do którego masz najmniejszy dostęp. Dobry opis różnych klas i przechodzenia przez nie można znaleźć w przewodniku AWS zatytułowanym Transitioning Objects. W moim przypadku zdecydowałem się stworzyć cykl życia, który przenosi każdy obiekt z Standard do Glacier po roku. Dostęp do Glacier jest znacznie tańszy niż standardowe archiwum S3 i pozwoli mi zaoszczędzić na kosztach przechowywania.

Gdy już stworzyłem wiadro S3 i ustawiłem moje parametry, S3 jest gotowe, abym mógł przesłać e-mail zgodny z rfc822, który uzyskałem z strumienia danych webhooka SparkPost Relay. Ale przed przesłaniem ładunku e-maila rfc822 do S3 muszę stworzyć unikalną nazwę pliku, którą użyję do przechowywania tego e-maila.

Dla unikalnej nazwy pliku zamierzam przeszukać treść e-maila w poszukiwaniu ukrytego id, które aplikacja wysyłająca umieściła w e-mailu i użyć tego id jako nazwy pliku. Istnieją bardziej eleganckie sposoby na pobranie connectorId z treści html, ale dla prostoty i jasności zamierzam użyć następującego kodu:

$start = strpos($htmlbody, $inputField);
if ($start !== false) {
    $start = strpos($htmlbody, 'value="', $start);
    if ($start !== false) {
        $start += 7; // Move past 'value="'
        $end = strpos($htmlbody, '"', $start);
        if ($end !== false) {
            $length = $end - $start;
            $UID = substr($htmlbody, $start, $length);
        }
    }
}

* zakładamy, że $inputField zawiera wartość "ArchiveCode" i została znaleziona w moim pliku config.php.

Z UID możemy następnie utworzyć nazwę pliku, która będzie używana w S3:

$fileName = $ArchiveDirectory . '/' . $UID . '.eml';

Teraz mogę otworzyć moje połączenie z S3 i przesłać plik. Jeśli spojrzysz na plik s3.php w repozytorium GitHub, zobaczysz, że potrzeba bardzo mało kodu, aby przesłać plik.

Moim ostatnim krokiem jest zarejestrowanie tego wpisu w tabeli MYSQL.

Przechowywanie metadanych w MySQL

Zebraliśmy wszystkie niezbędne dane w poprzednim kroku, więc krok przechowywania jest łatwy.  W tej pierwszej fazie postanowiłem zbudować tabelę z następującymi polami:

Pola metadanych MySQL

Pole

Cel

Data/godzina (auto)

Znacznik czasu, kiedy wpis został zapisany

Adres RCPT_TO

Docelowy adres e-mail dla zarchiwizowanej wiadomości

Znacznik czasu nagłówka DATE

Oryginalny czas wysłania e-maila

Znacznik nagłówka SUBJECT

Temat do indeksowania i wyszukiwania

Znacznik nagłówka FROM

Identyfikator nadawcy do wyszukiwania

Katalog S3

Ścieżka katalogu wewnątrz koszyka S3

Nazwa pliku S3

Unikalny plik .eml przechowywany w S3

Funkcja o nazwie MySQLLog w pliku aplikacji upload.php przechodzi przez niezbędne kroki, aby otworzyć połączenie z MySQL, wstrzyknąć nowy wiersz, przetestować wyniki i zamknąć połączenie. Dodaję jeszcze jeden krok dla pewności, a mianowicie zapisuję te dane do pliku tekstowego. Czy powinienem robić znacznie więcej logowania błędów? Tak. Ale chcę, aby ten kod działał lekko, aby mógł działać bardzo szybko. Czasami ten kod będzie wywoływany setki razy na minutę i musi być jak najbardziej efektywny. W przyszłych aktualizacjach dodam dodatkowy kod, który będzie przetwarzać błędy i wysyłać te błędy na adres e-mail administratora w celu monitorowania.

Zebraliśmy wszystkie niezbędne dane w poprzednim kroku, więc krok przechowywania jest łatwy.  W tej pierwszej fazie postanowiłem zbudować tabelę z następującymi polami:

Pola metadanych MySQL

Pole

Cel

Data/godzina (auto)

Znacznik czasu, kiedy wpis został zapisany

Adres RCPT_TO

Docelowy adres e-mail dla zarchiwizowanej wiadomości

Znacznik czasu nagłówka DATE

Oryginalny czas wysłania e-maila

Znacznik nagłówka SUBJECT

Temat do indeksowania i wyszukiwania

Znacznik nagłówka FROM

Identyfikator nadawcy do wyszukiwania

Katalog S3

Ścieżka katalogu wewnątrz koszyka S3

Nazwa pliku S3

Unikalny plik .eml przechowywany w S3

Funkcja o nazwie MySQLLog w pliku aplikacji upload.php przechodzi przez niezbędne kroki, aby otworzyć połączenie z MySQL, wstrzyknąć nowy wiersz, przetestować wyniki i zamknąć połączenie. Dodaję jeszcze jeden krok dla pewności, a mianowicie zapisuję te dane do pliku tekstowego. Czy powinienem robić znacznie więcej logowania błędów? Tak. Ale chcę, aby ten kod działał lekko, aby mógł działać bardzo szybko. Czasami ten kod będzie wywoływany setki razy na minutę i musi być jak najbardziej efektywny. W przyszłych aktualizacjach dodam dodatkowy kod, który będzie przetwarzać błędy i wysyłać te błędy na adres e-mail administratora w celu monitorowania.

Zebraliśmy wszystkie niezbędne dane w poprzednim kroku, więc krok przechowywania jest łatwy.  W tej pierwszej fazie postanowiłem zbudować tabelę z następującymi polami:

Pola metadanych MySQL

Pole

Cel

Data/godzina (auto)

Znacznik czasu, kiedy wpis został zapisany

Adres RCPT_TO

Docelowy adres e-mail dla zarchiwizowanej wiadomości

Znacznik czasu nagłówka DATE

Oryginalny czas wysłania e-maila

Znacznik nagłówka SUBJECT

Temat do indeksowania i wyszukiwania

Znacznik nagłówka FROM

Identyfikator nadawcy do wyszukiwania

Katalog S3

Ścieżka katalogu wewnątrz koszyka S3

Nazwa pliku S3

Unikalny plik .eml przechowywany w S3

Funkcja o nazwie MySQLLog w pliku aplikacji upload.php przechodzi przez niezbędne kroki, aby otworzyć połączenie z MySQL, wstrzyknąć nowy wiersz, przetestować wyniki i zamknąć połączenie. Dodaję jeszcze jeden krok dla pewności, a mianowicie zapisuję te dane do pliku tekstowego. Czy powinienem robić znacznie więcej logowania błędów? Tak. Ale chcę, aby ten kod działał lekko, aby mógł działać bardzo szybko. Czasami ten kod będzie wywoływany setki razy na minutę i musi być jak najbardziej efektywny. W przyszłych aktualizacjach dodam dodatkowy kod, który będzie przetwarzać błędy i wysyłać te błędy na adres e-mail administratora w celu monitorowania.

Podsumowując

W zaledwie kilku stosunkowo łatwych krokach, mogliśmy przejść przez pierwszą fazę budowy solidnego systemu archiwizacji e-maili, który przechowuje duplikaty e-maili w S3 i porównuje dane w tabeli MySQL.  To da nam fundament do reszty projektu, który będzie realizowany w kilku przyszłych postach.

W przyszłych rewizjach tego projektu oczekiwałbym:

  1. Przechowywać wszystkie zdarzenia dziennika oryginalnego e-maila

  2. Wysyłać błędy przechowywania do administratora, gdy wystąpi błąd przesyłania lub logowania

  3. Zmniejszyć złożoność kolektora.

  4. Dodaj interfejs użytkownika do przeglądania wszystkich danych

  5. Wspierać możliwość ponownego wysłania e-maila

W międzyczasie, mam nadzieję, że ten projekt był interesujący i pomocny dla Ciebie; miłego wysyłania.

W zaledwie kilku stosunkowo łatwych krokach, mogliśmy przejść przez pierwszą fazę budowy solidnego systemu archiwizacji e-maili, który przechowuje duplikaty e-maili w S3 i porównuje dane w tabeli MySQL.  To da nam fundament do reszty projektu, który będzie realizowany w kilku przyszłych postach.

W przyszłych rewizjach tego projektu oczekiwałbym:

  1. Przechowywać wszystkie zdarzenia dziennika oryginalnego e-maila

  2. Wysyłać błędy przechowywania do administratora, gdy wystąpi błąd przesyłania lub logowania

  3. Zmniejszyć złożoność kolektora.

  4. Dodaj interfejs użytkownika do przeglądania wszystkich danych

  5. Wspierać możliwość ponownego wysłania e-maila

W międzyczasie, mam nadzieję, że ten projekt był interesujący i pomocny dla Ciebie; miłego wysyłania.

W zaledwie kilku stosunkowo łatwych krokach, mogliśmy przejść przez pierwszą fazę budowy solidnego systemu archiwizacji e-maili, który przechowuje duplikaty e-maili w S3 i porównuje dane w tabeli MySQL.  To da nam fundament do reszty projektu, który będzie realizowany w kilku przyszłych postach.

W przyszłych rewizjach tego projektu oczekiwałbym:

  1. Przechowywać wszystkie zdarzenia dziennika oryginalnego e-maila

  2. Wysyłać błędy przechowywania do administratora, gdy wystąpi błąd przesyłania lub logowania

  3. Zmniejszyć złożoność kolektora.

  4. Dodaj interfejs użytkownika do przeglądania wszystkich danych

  5. Wspierać możliwość ponownego wysłania e-maila

W międzyczasie, mam nadzieję, że ten projekt był interesujący i pomocny dla Ciebie; miłego wysyłania.

Inne wiadomości

Przeczytaj więcej z tej kategorii

A person is standing at a desk while typing on a laptop.

Kompletna platforma oparta na sztucznej inteligencji, która rośnie wraz z Twoim biznesem.

A person is standing at a desk while typing on a laptop.

Kompletna platforma oparta na sztucznej inteligencji, która rośnie wraz z Twoim biznesem.