Сквозная задача на Исполнителе - часть первая (IMAP)

01.06.23

Разработка - Языки и среды

Поставили нам задачу - вынести на отдельный сервер функционал получения заказов от клиентов по электронной почте, парсинг полученных XLS в приемлемый вид и трансформация заказов в красивый JSON, понятный нашей учетной системе на 1С. Всю эту красоту желательно запустить в отдельном докер - контейнере, по возможности не тратя лицензии, поэтому отдельно стоящую конфигурацию на БСП отвергаем сразу. Можно было бы собрать всё на Apache Airflow или Apache NiFi, но решили попробовать реализовать всю логику без Open Source, будем делать свой ETL, с Исполнителем, который в версии 3.0 научился взаимодействовать с электронной почтой по IMAP. Начнем с середины - сначала напишем скрипты, а потом соберем их в рабочую конструкцию

Скачать исходный код

Наименование Файл Версия Размер
Сквозная задача на Исполнителе - часть первая (IMAP):
.SBSL 18,49Kb
0
.SBSL 18,49Kb Скачать

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

Установим Исполнитель версии (U), VSC с плагином и начнем немного нестандартно - объявим две структуры. Как и перечисления, это теперь нестандартная коллекция с более широкими возможностями, но в нашем случае сильно мудрить не будем, просто вложим одну в другую.

Для тех, кто совсем не знаком с исполнителем - для улучшение читаемости пару замечаний.

Строка=Строка+"новенькое" это Строка +="Новенькое"

в строке \ экранируем как \\

 

 
Объявление структур

 

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

Объявляем метод Скрипт() и стучим на наш IMAP сервер с почтой

 

 
 Главный метод Скрипт() начало

 

метод Скрипт() 
    пер Параметры = новый ПараметрыПодключенияImap(Сервер(), Порт(), новый АутентификацияПочтыПоПаролю(Логин(),Пароль()),ПараметрыЗащищенногоСоединения()) //Отступление первое
    исп Соединение = новый СоединениеImap(Параметры)
    пер Входящие = Соединение.ПолучитьКаталог(ВидКаталогаПочты.Входящие)
    Входящие.Открыть(РежимОткрытияКаталогаImap.Чтение)
    пер Письма = ПолучитьВсеПисьма(Входящие) // зависит от потребности, ниже есть метод ПолучитьНепрочитанныеПисьма()

 


Логин() и т.д. можно, конечно, было бы передать в параметрах к методу Скрипт, но мы для удобства пока вынесем в отдельные методы
 

 
 Методы параметров подключения

 
Обещанное в комментариях к коду отступление первое - насколько я понимаю, подключается Исполнитель сейчас только к IMAPS (порт 993/TCP/TLS) - а вот в STARTTLS на порту imap (порт 143/TCP) пока не умеет - прошу учитывать.

И второй момент - если вы подключаетесь к своему внутреннему IMAP серверу, то вполне можете столкнуться с ошибкой "The server selected protocol version TLS10 is not accepted by client preferences [TLS13, TLS12]" - В таком случае надо обратиться к файлу  java.security  и в параметре  jdk.tls.disabledAlgorithms удалить TLS10 или TLS11 (Прокатывает только в исполнителе U версии).

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

 

 
 Получаем письма с сервера в режиме Чтение


Теперь продолжим наш "главный скрипт"
 

 
 Главный Скрипт() продолжение

 

 для текПисьмо из Письма // Начинаем обход полученной коллекции. Будет немного масла масляного - у объекта ПисьмоВСоединенииImap надо будет обратиться к унаследуемому свойству Письмо
        пер ОтправительЗначение = НайтиЕмейл(текПисьмо.Письмо.Отправитель.Адрес.ВСтроку()) // здесь у нас MIME BASE64 но мне ФИО отправителя не нужно, поэтому безжалостно выкусываю 
        пер ДатаИВремяОтправленияЗначение = текПисьмо.Письмо.ДатаОтправления.ВСтроку()
        пер ИндексОтсечкиДаты = ДатаИВремяОтправленияЗначение.Найти("T")
        пер ДатаОтправленияЗначение = ДатаИВремяОтправленияЗначение.ПодстрокаСНачала(ИндексОтсечкиДаты) // От даты и время оставляем дату - по желанию
        пер ИДСообщенияЗначение = текПисьмо.Письмо.ИдСообщения.ВСтроку()
        пер ИндексОтсечки = ИДСообщенияЗначение.Найти("@")
        пер ИДСообщенияКраткоеЗначение = ИДСообщенияЗначение.ПодстрокаСНачала(ИндексОтсечки) // оставили от идентификатора сообщения кусок до @ - потом идет адрес IMAP сервера
        ИДСообщенияКраткоеЗначение = ИДСообщенияКраткоеЗначение.Заменить("<","") // и убрали артефакты
        пер УИДЗначение = текПисьмо.Uid.ВСтроку() //Уникальный идентификатор письма, не глобальный - именно этому получателю
        пер ЕмейлыТекстаЗначение = НайтиЕмейл(текПисьмо.Письмо.ПолучитьТекстКакСтроку())  //Если письмо переслали для "молотилки" - нужные адреса электронной почты будут в тексте письма - выбираем
        пер ИмяФайлаJSON = ДатаОтправленияЗначение +"_"+ ОтправительЗначение +"_"+ УИДЗначение + ".txt"

 

 Если посмотреть на всю эту красоту в отладке, то в некоторых переменных (например, ОтправительЗначение) может иметь вид 

=?UTF-8?B?лалалаBase64код ==? normalniy@email.ru.  Это так называемый MIME BASE64 и пришла пора для Отступление 2

Исполнитель сейчас умеет немного в Кодировку и даже синглтон для этого есть подходящий - но в данном случае он сумеет только разобрать из BASE64 зашитый там UTF. Если же там WIN1251 или 1252 - ну не повезло. В случае с отправителем нам для этой задачи неважно раскодировать ФИО  отправителя, email-то там "красивый" незакодированный, но дальше все-таки придется.

Поэтому в случае с отправителем и телом письма - просто выкусываем оттуда регулярками все email, для идентификации клиента этого вполне достаточно. Сложного тут ничего нет - все понятно из комментариев

 
 Получаем email из текста

 

метод НайтиЕмейл(ТекстДляПоиска:Строка): Строка
    знч ОбразецДляПоискаСочетаний = '([A-Za-z0-9]+[.\-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+' //Это не строка - тип "Образец" для регулярки, объявляется литералом
    знч РезультатПоиска = ОбразецДляПоискаСочетаний.НайтиСовпадения(ТекстДляПоиска) //получаем массив email и обходим слегка допилив напильником
    пер НайденныйЕмейл = ""
        для ОчереднойРезультат из РезультатПоиска
            НайденныйЕмейл += ОчереднойРезультат.Значение()+ ","
        ;       
        если НайденныйЕмейл.ЗаканчиваетсяНа(",")
            НайденныйЕмейл = НайденныйЕмейл.ПодстрокаСНачала(НайденныйЕмейл.Длина()-1)  //Уберем последнюю запятую для красоты 
        ;
        НайденныйЕмейл = НайденныйЕмейл.Заменить("mailto:","") //если текст писльма HTML то возможны подобные артефакты, убираем, не стал добавлять в регулярку для читаемости
    возврат НайденныйЕмейл
;

 

Дальше начинаем работу с вложениями почтового сообщения

 
 Работа с Вложениями

 

  пер НомерВложенияЗначение = 1
        пер МассивВложенийЗначение:Массив<ОписаниеВложения>
            для текВложение из текПисьмо.Письмо.Вложения
                пер ИмяВложенияНекодированноеЗначение = текВложение.ВСтроку()
                пер ПрограммноеИмяВложенияЗначение = ДатаОтправленияЗначение +"_"+ ОтправительЗначение +"_"+ УИДЗначение+"_attachment_"+ НомерВложенияЗначение
                пер ИмяВложенияраскодированноеЗначение = ПытаемсяРаскодироватьMIME_BASE64(ИмяВложенияНекодированноеЗначение) //Отступление второе
                пер ТипСодержимогоЗначение = текВложение.ТипСодержимого // MIME-тип вложения
                пер СохраняемФайл = Истина
                // Дальше немного жести - всеми правдами и неправдами пытаемся понять, нужный ли это файл - Отступление второе

 

 

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

 
 Метод раскодирования MIME BASE64

 

метод ПытаемсяРаскодироватьMIME_BASE64(ТекстДляДекодирования:Строка): Строка // магия из второго отступления
    пер ИмяФайлаДекодированное = ""
    знч ОбразецДляПоискаСочетаний = "=?utf-8?B?"
    ТекстДляДекодирования = ТекстДляДекодирования.Заменить(Строка ="=?UTF-8?B?",Замена = ОбразецДляПоискаСочетаний) // Так как мне лень использовать в Разделить() регулярку - делаю замену, приходит и так и так
    знч ПраваяЧасть ="?="
    знч ЕстьКодировкаUTF = ТекстДляДекодирования.Содержит(Строка = ОбразецДляПоискаСочетаний, ИгнорироватьРегистр = Ложь)  
    если ЕстьКодировкаUTF // из  остальных кодировок исполнитель пока Base64 преобразовывать не умеет
        знч РезультатПоиска = ТекстДляДекодирования.Разделить(Разделитель = ОбразецДляПоискаСочетаний, ВключаяПустые = Ложь) 
    
        для текСтрока из РезультатПоиска

            пер ПоискПоПравойЧасти = текСтрока.Разделить(Разделитель = ПраваяЧасть, ВключаяПустые = Ложь ) //код несовершенен - уничтожает значимую незакодированную правую часть
            если ПоискПоПравойЧасти.Размер() > 1          // Никакого теперь "Количество()" - привыкаем 
                для ТекМалаяСтрока из ПоискПоПравойЧасти //если нужна значимая правая часть - раскодируйте только первую строку массива а вторую просто конкатенируйте не трогая
                    ИмяФайлаДекодированное += Кодировки.Base64.ДекодироватьВСтроку(ТекМалаяСтрока.ВСтроку())
                ;
            иначе
                ИмяФайлаДекодированное += Кодировки.Base64.ДекодироватьВСтроку(текСтрока.ВСтроку())  
            ;   
        ;
        // Консоль.Записать(ИмяФайлаДекодированное)
        возврат ИмяФайлаДекодированное
    иначе 
        возврат ТекстДляДекодирования 
    ;

;

 

 

Так как имя файла нам могли прислать и в той кодировке, которую исполнитель пока не умеет - можно заглянуть в MIME тип вложения. Ну и если это экселевская книга - сохраняем на диск

 

 
 Главный метод Скрипт() продолжение

 


                если ИмяВложенияраскодированноеЗначение.Содержит(".xlsx") 
                    ПрограммноеИмяВложенияЗначение +=".xlsx"
                иначе  если ИмяВложенияраскодированноеЗначение.Содержит(".xls")
                    ПрограммноеИмяВложенияЗначение +=".xls"
                иначе если ИмяВложенияНекодированноеЗначение.Содержит(".xlsx")
                    ПрограммноеИмяВложенияЗначение +=".xlsx"
                иначе если ИмяВложенияНекодированноеЗначение.Содержит(".xls")
                    ПрограммноеИмяВложенияЗначение +=".xls" 
                иначе если ТипСодержимогоЗначение.Содержит("excel")   
                    ПрограммноеИмяВложенияЗначение +=".xls"  
                иначе если ТипСодержимогоЗначение.Содержит(".xlsx")
                    ПрограммноеИмяВложенияЗначение +=".xlsx"
                иначе если ТипСодержимогоЗначение.Содержит(".xls")
                    ПрограммноеИмяВложенияЗначение +=".xls" 
                иначе
                    СохраняемФайл = Ложь // все что не имеет отношения к Excel не сохраняем
                ;
                если СохраняемФайл     
                    пер ФайлДляВложения = новый Файл(КаталогВыгрузки() + ПрограммноеИмяВложенияЗначение) 
                        если не ФайлДляВложения.Существует() 
                            исп ПотокЗаписи = ФайлДляВложения.ОткрытьПотокЗаписи()
                            ПотокЗаписи.Записать(текВложение.Данные)
                            ПотокЗаписи.Закрыть() // Файл сохранили - теперь неплохо бы немного раскрыть тему - как он назывался, чей и прочее прочее. 
                        ;
                ;

 

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

 
 Главный метод Скрипт() окончание

 

 знч СериализованнаяЗаписьОВложении = новый ОписаниеВложения( ИмяВложенияНекодированное = ИмяВложенияНекодированноеЗначение,
                                                                            ИмяВложенияРаскодированное = ИмяВложенияраскодированноеЗначение,      
                                                                            ПрограммноеИмяВложения = ПрограммноеИмяВложенияЗначение,
                                                                            КаталогВыгрузки = КаталогВыгрузки(),
                                                                            НомерВложения = НомерВложенияЗначение,
                                                                            ТипСодержимого = ТипСодержимогоЗначение)   // помните мы в начале структуры создавали - вот и пригодились             
         
                МассивВложенийЗначение.Добавить(СериализованнаяЗаписьОВложении) // строго говоря в Исполнителе через Добавить() с массивом работать не рекомендуют но мы никому не расскажем
                НомерВложенияЗначение += 1
             
            ;
           
            знч СериализованноеПисьмо = новый СтруктураСериализованногоПисьма(Отправитель = ОтправительЗначение,
                                                                                ДатаОтправления = ДатаОтправленияЗначение,
                                                                                ИДСообщения = ИДСообщенияЗначение,
                                                                                УИД = УИДЗначение,
                                                                                ЕмейлыТекста = ЕмейлыТекстаЗначение,
                                                                                МассивВложений = МассивВложенийЗначение) //А это шапочка письма
            пер ТекстJSON = СериализацияJson.ЗаписатьОбъект(СериализованноеПисьмо) // Легким двежением руки созданные структуры сериализуем в JSON

            пер ФайлJSON = новый Файл(КаталогВыгрузки() + ИмяФайлаJSON) 
             если не ФайлJSON.Существует() 
                исп ПотокЗаписи = ФайлJSON.ОткрытьПотокЗаписи()
                    ПотокЗаписи.Записать(ТекстJSON)
                    ПотокЗаписи.Закрыть()
            ;

                //   Консоль.Записать(ТекстJSON.ВСтроку())

        
    ;
    Входящие.Закрыть()
    Скрипт.ЗавершитьРаботу() //без этого иногда может виснуть - но вообще не обязательно
;

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

Разбирать их будем в следующей статье, если тема  будет интересна.

 

Исполнитель импорт IMAP

См. также

Как вызвать скрипты на python в 1С по технологии NativeAPI

Языки и среды Платформа 1С v8.3 Бесплатно (free)

Будем писать свои скрипты на питоне и запускать их на 1С.

15.04.2024    1311    YA_418728146    11    

50

Зачем нам 1С:Элемент

Мобильная разработка Языки и среды Бесплатно (free)

Flutter может быть использован с 1С:Предприятием для разработки кроссплатформенных мобильных приложений, обеспечивая единый интерфейс и функциональность на устройствах под управлением iOS и Android. Это позволяет создавать приложения с высокой производительностью благодаря использованию собственного движка рендеринга Flutter. Интеграция Flutter с 1С:Предприятием позволяет создавать мобильные приложения любого уровня сложности, интегрировать их в корпоративные информационные системы, а также реализовывать бизнес-логику

19.03.2024    9351    ROk_dev    67    

41

(Не) Строгая типизация 1С

Языки и среды Платформа 1С v8.3 Бесплатно (free)

Существует множество языков программирования, и каждый имеет свои особенности по работе с типами данных. Слабые, явные, динамические и другие... Но кто же здесь 1С и почему с приходом "строгой" типизации EDT 1С-программистам стоит задуматься над изменением своих привычек.

16.01.2024    4556    SeiOkami    21    

55

Простое приложение на Dart

Языки и среды Бесплатно (free)

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

08.08.2023    3347    acvatoris    6    

14

Статический анализатор кода 1С на Си

Языки и среды Платформа 1С v8.3 Россия Бесплатно (free)

Написание статического анализатора для 1С традиционным способом на Си.

30.06.2023    3046    prohorp    15    

12

1С# - Расширяем код 1С кодом на C#

Языки и среды Инструментарий разработчика Платформа 1С v8.3 Конфигурации 1cv8 Абонемент ($m)

Вставки кода на C# внутри кода на 1С.

7 стартмани

07.04.2023    9452    4    SerVer1C    56    

43

Независимая разработка совместимых компонент на ORM 1С – миф или истина где-то в аннотациях Java?

Языки и среды Платформа 1С v8.3 Бесплатно (free)

При работе с 1С ORM (object relation mapping) все время преследует ощущение постоянного создания монолитного приложения — один раз привязался к какой либо сущности (например, справочник Контрагенты), и весь код заполнен ссылками на эту конкретную реализацию. Можно ли независимо разрабатывать в ORM совместимые между собой справочник «Контрагентов» и использующий его документ «Платежное поручение», но при этом избежать жестких зависимостей? Спасут ли нас микросервисы? Пример на аннотациях Java демонстрирует, как это возможно делать.

13.03.2023    1067    1CUnlimited    0    

2

xPath в 1С

Файловый обмен (TXT, XML, DBF), FTP Языки и среды Платформа 1С v8.3 Бесплатно (free)

Опыт работы методами языка xPath в 1С.

04.03.2023    5021    DemetrKlim    40    

46
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. SerVer1C 750 01.06.23 14:30 Сейчас в теме
Для чего нужны именно методы для параметров подключения? Нельзя обойтись константами/переменными?
+
2. kembrik 10 01.06.23 14:35 Сейчас в теме
(1) Можно конечно - как только функционал протестирован убираем их непосредственно в параметры метода Скрипт() и вызываем уже как параметры командной строки. Методами сделано исключительно для того чтобы в теле основного скрипта не было литералов

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

метод Скрипт(СоответствиеПараметров: Соответствие<Строка, Строка>): Число


    пер Параметры = ПолучитьПараметры(СоответствиеПараметров)
    
    ОбщиеПроверитьОбязательныеПараметры(Параметры, [
              новый СтруктураПроверкиПараметра("username", Параметры.ИмяПользователя, () -> (не Параметры.ИмяПользователя.Пусто())),  
              новый СтруктураПроверкиПараметра("password", Параметры.Пароль, () -> (не Параметры.Пароль.Пусто())),
              
        ]
;

метод ПолучитьПараметры(СоответствиеПараметров: Соответствие<Строка, Строка>): СтруктураПараметров

    пер Параметры = новый СтруктураПараметров()
   
    Параметры.ИмяПользователя = ОбщиеЗначениеПараметраСтрока(СоответствиеПараметров, "username")
    Параметры.Пароль = ОбщиеЗначениеПараметраСтрока(СоответствиеПараметров, "password")
 
    
    возврат Параметры

;
Показать
+
Оставьте свое сообщение