Как несисадмин bash писал, или IMAP to API при помощи Fetchmail

e3f1490d723a3d862443e8a5f6c978a6.png

Как получить правильный ответ от Chat GPT — так же как и в реальной жизни — задать правильный вопрос. Какой вопрос правильный? Да кто ж его знает, но найти его проще, чем просто копаться на stackoverflow. Про это и статья… Ну и помимо этого под катом работающий скрипт, который слушает почту и пересылает письма с вложениями и русским языком на API endpoint.

Вообще, мы делаем open source конструктор корпоративных приложений, для таких же полу-программистов типа меня. И от одного из пользователей был вопрос, как можно слушать почтовый ящик и получать в конструктор пришедшие письма с вложениями. Такого функционала у нас базово нет, но есть настраиваемые API эндпойнты. Разбираться с библиотеками python/php (далее выбрать по вкусу) не хотелось, так как для меня это, признаю, тяжело, и поэтому решил на удачу спросить GPT про маленький скриптик для bash, чтобы проверял и пересылал, на крон его поставить, и было бы вообще замечательно. На удивление, он выдал готовый рецепт, который, конечно же, не работал, но в котором были зацепки в виде fetchmail, procmail и munpack (с которым в итоге получилось, но криво, и пришлось все переделать), про которые я никогда не слышал и не услышал бы, если бы не GPT. Так как какой-то не совсем нулевой опыт у меня в bash был, я решил разобраться поподробнее.

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

Итак, поехали, ставим нужное ПО:

sudo apt install fetchmail procmail uudeview

uudeview нашелся не сразу. Чат мне настойчиво предлагал munpack, но тот не разбирает MIME-кодированные названия файлов, когда они, например, на русском, а создает файл на диске по имени base64, который лежит в теге MIME-энкода. Но так как там могут быть /, то часто вместо создания файла, он создавал папку и файл. Сначала я написал обработчик замены символов base64 на безопасные, но это был дополнительный дурацкий шаг. Спас habr, на котором я нашел эту статью: https://habr.com/ru/articles/806755/ Но к этому моменту я уже знал, что надо искать fetchmail и обработку через procmail и решил попробовать удачу. Удача оказалась удачной.

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

Создаем файл конфигурации fetchmailrc:

nano ~/.fetchmailrc
poll imap.host.com protocol IMAP port 993
  user "email@host.com" password "some_pass_here" is "local-user" here
  mda "/usr/bin/procmail"
  ssl

Со стандартными настройками fetchmail у чата все в порядке, но если какие-то более продвинутые опции, то мне так и не удалось добиться от него работающего конфига. Как правильно указывать options, он походу не знает. Но я догадываюсь почему — примеров в интернете реально немного.

Далее я намучился с вызовом mda "/usr/bin/procmail", так как во всех инструкциях в интернете это указано как mda "/usr/bin/procmail -d %T", то есть доставлять почту локальному пользователю. На этом же настаивал и чат. Но реально, в этом случае это оказалось не нужно, так как нам просто нужно запустить скрипт в пайпе, которому на стандартный ввод передать текст письма и все. В этом месте пришлось долго экспериментировать, так как когда я говорил чату, что не работает, он пытался такие вензеля написать в конфиге, что мне становилось страшно.

Устанавливаем права на fetchmailrc:

chmod 700 ~/.fetchmailrc

Создаем конфиг procmail:

nano ~/.procmailrc
MAILDIR=$HOME/Mail
DEFAULT=$MAILDIR/mbox
LOGFILE=$MAILDIR/procmail.log

:0
* ^From.*
| $MAILDIR/extract.sh

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

Устанавливаем права на procmailrc:

chmod 600 ~/.procmailrc

Создаем папку Mail и внутри data:

mkdir -p ~/Mail/data && cd ~/Mail

Письма будут лежать в data, чтобы потом удалять оттуда старые.

Создаем скрипт обработки писем (ближе к концу найдите MY_KEY и замените на ваш, если хотите проверять ключ на эндпойнте, а также замените YOUR_HOST на ваш эндпойнт):

nano ~/Mail/extract.sh
#!/bin/bash

EMAIL=$(cat)
DATE=`date +%s`
RANDOM_NUM=$(echo "$RANDOM" | head -c 3)
UNIQ_NAME="email_${DATE}_${RANDOM_NUM}"
DIR="$HOME/Mail/data/$UNIQ_NAME"
JSON="$DIR/$UNIQ_NAME.json"

mkdir -p $DIR

echo "$EMAIL" > "$HOME/Mail/data/$UNIQ_NAME.mail"

echo "$EMAIL" | uudeview -i +a +o -t -p $DIR -

decode_and_concatenate() {
  local input_string="$1"
  local result=""

  for part in $input_string; do
    decoded_part=$(echo $part | awk -F '[?]' '{print $4}' | openssl enc -base64 -d -A)
    result+="$decoded_part"
  done

  echo "$result"
}


FROM=$(echo "$EMAIL" | formail -c -x From: | awk -F'[<>]' '{print $2}')

FROM_NAME=$(echo "$EMAIL" | formail -c -x From: | awk -F'<|>' '{print $1}' | sed 's/^[ \t]*//;s/[ \t]*$//' | while read -r name_line; do
    if [[ $name_line == =?* ]]; then
      name_clear=$(echo "$name_line" | sed 's/  / /g')
      decode_and_concatenate "$name_clear"
    else
      echo "$name_line"
    fi
  done | tr -d '\n' | base64 -w 0)

TO=$(echo "$EMAIL" | formail -x To: | sed 's/^[ \t]*//;s/[ \t]*$//;s/"//g' | sed 's/.*<\([^>]*\)>.*/\1/')

SUBJECT=$(echo "$EMAIL" | formail -c -x Subject | sed 's/^[ \t]*//;s/[ \t]*$//' | while read -r sub_line; do
    if [[ $sub_line == =?* ]]; then
      sub_clear=$(echo "$sub_line" | sed 's/  / /g')
      decode_and_concatenate "$sub_clear"
    else
      echo "$sub_line"
    fi
  done | tr -d '\n' | base64 -w 0)

if [ -f "$DIR/UNKNOWN.001" ]; then
  BODY=$(base64 -w 0 "$DIR/UNKNOWN.001")
else
  if formail -c -x Content-Type: < "$DIR/0001.txt" | grep -qv "multipart"; then
    BODY=$(formail -I "" < "$DIR/0001.txt" | base64 -w 0)
  else
    BODY=$(base64 -w 0 "$DIR/0001.txt")
  fi
fi

FILES=()

for FILE_PATH in "$DIR"/*; do
    FILE_NAME=$(basename "$FILE_PATH")
    FILE_NAME_BASE=$(echo "$FILE_NAME" | tr -d '\n' | base64 -w 0)
    if [[ ! "$FILE_NAME" =~ ^UNKNOWN\.[0-9]+$ ]] && [[ ! "$FILE_NAME" =~ ^000[0-9]+\.txt$ ]]; then
        FILE_CONTENT=$(base64 -w 0 "$FILE_PATH")
        FILES+=("{\"name\":\"$FILE_NAME_BASE\",\"filestringbase64\":\"$FILE_CONTENT\"}")
    fi
done

FILES_JSON=$(printf ",%s" "${FILES[@]}")
FILES_JSON="[${FILES_JSON:1}]"

echo "{\"email_id\":\"$UNIQ_NAME\",\"from\":\"$FROM\",\"from_name\":\"$FROM_NAME\",\"to\":\"$TO\",\"subject\":\"$SUBJECT\",\"body\":\"$BODY\",\"files\":$FILES_JSON}" > $JSON

KEY="MY_KEY"

curl -d @"$JSON" -H "Content-Type: application/json" -H "Authorization: $KEY" https://YOUR_ENDPOINT

rm -r $DIR

Делаем исполняемым:

chmod +x ~/Mail/extract.sh

Выполняем:

fetchmail -v

Что делает:

  • получает письма с подключенного ящика через fetchmail;

  • передает в procmail;

  • procmail передает содержимое письма со всеми вложениями в скрипт (возможно, что можно пропустить procmail, но я не экспериментировал);

  • скрипт:

    • сохраняет исходник письма в ~/Mail/data вида email_timestamp_random;

    • достает файлы из письма и временно их сохраняет;

    • достает тему письма, от кого, кому и расшифровывает их, если они в MIME-encode, и шифрует в base64;

    • проверяет, как было упаковано тело письма, достает его и шифрует в base64;

    • в цикле проходит по всем вложениям, сохраненным uudeview, и шифрует все в base64;

    • сохраняет во временный файл;

    • отправляет на эндпойнт json такого вида (в raw):
      {
      "to": "...",
      "body": "...",
      "from": "...",
      "subject": "...",
      "email_id": "...",
      "from_name": "...",
      "files":[{"name":"...","filestringbase64":"..."}]
      }

    • удаляет временные файлы;

Дальше мы уже все на той стороне распаковываем и кладем, куда нам нужно.

Дисклеймер: тестировал на том, что приходит от Gmail, Yandex, Mail и еще чего-то. Но могут быть варианты, которые я не учел, имея ограниченный пул для тестирования!

Когда все протестировали, можно поставить на крон получение, удаление оригиналов старше 3-х дней и очистку лога:

crontab -e
* * * * * fetchmail -s && find ~/Mail/data/ -type f -mtime +3 -exec rm {} \; && tail -n 1000 ~/Mail/procmail.log > ~/Mail/procmail.log.tmp && mv ~/Mail/procmail.log.tmp ~/Mail/procmail.log

Теперь немного про скрипт и что там пытался написать чат

Может быть для профессионала и сейчас этот скрипт выглядит странненько, но то, что предлагал чат, местами было еще более странненько. Так как полностью на старте описать все нюансы не получается, просто потому, что они еще неизвестны, приходится формулировать задачу вида «эээ, нууу, сделай мне скрипт, который что-то берет и куда-то отправляет» (это условно). Получаем что-то и начинаем его перерабатывать. Что я делал:

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

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

Приблизительно как человек — человек тоже помнит неидеально, но в отличие от человека, чат не пытался подсмотреть в инструкции. Когда я подкидывал ему контекста в виде вывода --help или manpage по тем пакетам, которые он предлагал использовать — результат улучшался.

Что бы проверить через командную строку просил оформить в однострочном варианте — он это делал хорошо.

Третье: со всем этим багажом проверял логику и последовательность, так как чат, большой любитель накруговертить. Особенно он разгонялся, когда я весь его код засовывал, просил исполнить, получал ошибку и скидывал ему ошибку и просил исправить. Почти всегда при исправлении результат ухудшался. Но просто попросить его пояснить ошибку и потом самому понять, где логично поправить — работало ок. Понятно, что в этом случае мы не просто копипастим код, а учимся, как он там на самом деле работает, и это может быть тяжело и лениво.

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

Пятое: часто очищал контекст (стартовал новую ветку). Есть такая тема, что чат походу самоубеждается через контекст, в том числе через тот, который сгенерировал сам. То есть если он сгенерировал фигню и потом ее еще разок повторил в рамках одной беседы, то все, он будет думать, что это правда и генерировать все последующее, включая туда эту фигню. Люди тоже так делают, но им нужно чуть больше времени, чтобы поверить в свой фантом, а чат это делает с полпинка. Я думаю, существенный прогресс в нейросетях наступит, когда их научат сомневаться.

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

В качестве заключения

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

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

© Habrahabr.ru