A place users can post their projects. If you have a small project and would like your own dedicated place to post and have others chat about it then this is your spot.

User avatar
By kejlycz
#61432 Hi, I've bought Wemos D1 mini to build mail notifier to get email notifications without turning on the computer. It's my first program, so please be gentle to me, improvement suggestions are welcome.
I've tried to comment functions, but basically:
- it connects to Wi-Fi - selection from more networks from array, I use the box on more places
- white LED flash - step started
- green LED flash - receiving data from mail server
- red LED on - unread email exists on server
- sound play via pcm - notifies every new email just once, after notifying of new email just red LED is set without sound, until new mail arrives
- blue LED on - mail from VIP email address

IMAP steps 1 (LOGIN) and 2 (LIST, isn't needed and can be safely removed) are processed just once, other steps are repeated in loop. Step time should be longer, than sound played (or sound will be cut), receiving of new email (FETCH) can take more than one step (but program waits until email is fully received), if you want, you can shorten the received response (I'm waiting for 'OK Fetch completed', this is the safest way to, but if you find length of your messages, so it also contains 'From:' field, which is used for VIP mail recognition). In step 5, the FETCH is processed for new mail, even if no VIP mail check is disabled, then it's really not needed and you can disable it (but be careful to include the 'unread_imaps.uns" stuff).
Informations about number of emails, last played pcm notification and count of new emails (just estimation, because IMAP doesn't report count of new emails, so it's just roughly calculated as EXISTS - UNSEEN + 1), so even after reset ESP knows last played notification and don't play sound, if it was already played for same UNSEEN, NEW and so on. The program runs in loop, but sometimes it crashes (be sure to use init.lua, if you want to use it for longer period), if someone can give me hint, why it crashes, I'll be happy.
I haven't found way, how to check certificates (always reported, that certificate doesn't match), so just encrypted connection to server on port 993 is established without checking of certificates. Port can be changed to insecure IMAP (IMAP_PORT and imap_socket = net.createConnection...), but I recommend to keep SSL on (this probably needs firmware with TLS included).
If you don't want to check emails in loop, but just once, just uncomment step 8 to close the connection to server, delete the last 'else' and stop timer 3.

Code: Select allMY_EMAIL = "user@na.me" --IMAP username
EMAIL_PASSWORD = "mailpassword" --IMAP password

IMAP_SERVER = "mail.server.com" --IMAP server name
IMAP_PORT = 993 --IMAP port (change to 143 for not secure connection, 993 for IMAPS)

vipcheck = true -- check VIP mail and set blue LED (for common mails red LED is set)
vipmail = nil
letsplay = true
pcmfile = "wavsound.u8" -- convert to wav "sox wavsound.wav -r 8000 -b 8 -c 1 wavsound.u8" (-r Supported sample rates are 1000, 2000, 4000, 5000, 8000, 10000, 12000, 16000)
vip_sender = "my@lovely.gf" --VIP sender (just one, I don't have more girlfriends;-)

APTABLE =
{
  home = { "", "" },--AP array 'descriptor = {"SSID", "SSID_PASSWORD"'}
  work = { "", "" }
}

count = 0
atcount = 1

debug = false -- show at responses and other debug stuff, affects performance and functions processing, use with caution!!!
response_processed = false
response = ""
lastplayed = nil

ledgreen=1 -- AT notification
ledred=2 -- unread mail notification
ledblue=4 -- unread vip mail notification
ledwhite=3 --step notification
pcmpin=5 -- pcm output

tmr.stop(1)
tmr.stop(2)
tmr.stop(3)

gpio.mode(ledgreen, gpio.OUTPUT)
gpio.mode(ledred, gpio.OUTPUT)
gpio.mode(ledblue, gpio.OUTPUT)

----PCM thing starts here
function cb_drained(d)
  file.seek("set", 0)
end

function cb_stopped(d)
  file.seek("set", 0)
  file.close()
  drv = nil
end

function cb_paused(d)
--
end

function play()
  file.open(pcmfile, "r")
  drv = pcm.new(pcm.SD, pcmpin)
  if file.open(pcmfile, "r") then
    drv:on("data", function(drv) return file.read() end)
  end
  drv:on("drained", cb_drained)
  drv:on("stopped", cb_stopped)
  drv:on("paused", cb_paused)
  drv:play(pcm.RATE_16K) -- Supported sample rates are pcm.RATE_1K, pcm.RATE_2K, pcm.RATE_4K, pcm.RATE_5K, pcm.RATE_8K, pcm.RATE_10K, pcm.RATE_12K, pcm.RATE_16K)
end
---- PCM thing ends here

function aplist(apfound)
  for o, d in pairs(APTABLE) do
    if d[1] == apfound then
      SSID = d[1]
      SSID_PASSWORD = d[2]
    end
  end
--do nothing
end

function flash(led, delay)
  gpio.write(led, gpio.HIGH)
  i=0
  while i < delay*100 do
    i = i + 1
  end
  gpio.write(led, gpio.LOW)
end

function atdisplay(imap_socket, response)
-- Correct responses are:
--   "a1 OK [CAPABILITY..........] Logged in"
--   "ax OK ............. completed."
-- In step 5 (FETCH) whole ending of response 'OK Fetch completed' is checked to avoid detection of 'completed' in email body
  response_processed = false
  flash(ledgreen,100)
  if (debug==true) then
    print("atdisplay start "..count)
  end

responseend = string.sub(response, -40) --just in case server supports lot of ptotocols
  if count == 0 and (string.match(responseend,'IMAP') ~= nil) then
    response_processed = true
    print("IMAP/IMAPS supported")
  elseif count == 1 and (string.match(responseend,'Logged') ~= nil) then
    response_processed = true
  elseif count ==5 and (string.match(responseend,'OK Fetch completed') ~= nil) then
    response_processed = true
  elseif count > 1 and (string.match(responseend,' completed') ~= nil) then
    response_processed = true
  end
  if (debug==true) then
    print("response = ", response)
    print("response processed = ", response_processed)
    print("atdisplay end ", count)
  end

  fromstart = string.find(response,'From: "')
  if fromstart ~= nil then
    fromcut = string.sub(response, fromstart, fromstart+200)
    vipmail = string.find(fromcut,vip_sender)
  end

  _G.response = response
end

function imapcheck()
  if (debug==true) then
    print (".")
  end
  if(response_processed == true) then
    if (debug==true) then
      print ("..")
    end
    flash(ledwhite,200)
    count = count + 1
    if(count == 1) then
      imap_socket:send("a" ..atcount.. " LOGIN " ..MY_EMAIL.. " " ..EMAIL_PASSWORD.. "\r\n")   
      imap_socket:on("receive", atdisplay)
      print(count.. " LOGIN")
      atcount = atcount + 1
    elseif(count == 2) then
      imap_socket:send("a" ..atcount.. " LIST \"\" \"*\"\r\n") -- not really needed, every mail has INBOX
      imap_socket:on("receive", atdisplay)
      print(count.. " LIST - just for fun")
      atcount = atcount + 1
    elseif(count == 3) then
      imap_socket:send("a" ..atcount.. " EXAMINE INBOX\r\n")
      imap_socket:on("receive", atdisplay)
      print(count.. " EXAMINE INBOX")
      atcount = atcount + 1
    elseif(count == 4) then
      _, _, exists = string.find(_G.response,"([0-9]+) EXISTS(\.)")
      unseenfound = string.find(_G.response,"UNSEEN")
      print(count.. " looking for unread emails")
      if unseenfound ~= nil then
        unseenstart = unseenfound+7
        unseenstring = string.sub(_G.response, unseenstart, unseenstart+7)
        unseenend = (string.find(unseenstring,"]")+unseenstart-2)
        firstunseen = tonumber(string.sub(_G.response, unseenstart, unseenend))
        newcount = exists - firstunseen + 1
        print ("  you have " ..newcount.. " unread email/s (might be wrong due to IMAP limitations)")
      end
    elseif(count == 5) then
      print(count.. " seen some some unseen messages?")     
      if firstunseen ~= nil then
        imap_socket:send("a" ..atcount.. " FETCH " ..firstunseen.. " BODY[]\r\n")
        imap_socket:on("receive", atdisplay)
        print("  FETCH " ..firstunseen)
        atcount = atcount + 1
        if file.exists("unread_imaps.uns") then
          if file.open("unread_imaps.uns", "r+") then
            cont = file.read()
            oldexists=tonumber(string.sub(cont, 0,string.find(cont,",")-1))
            oldfirstunseen=tonumber(string.sub(cont, string.find(cont,",")+1,string.find(cont,",")+string.find(string.sub(cont,(string.find(cont,",")+1)),",")-1))
            oldlastplayed=tonumber(string.sub(cont, string.find(cont,",")+(string.find(string.sub(cont,(string.find(cont,",")+1)),",")+1) , string.len(cont) ))
            oldnewcount = oldexists - oldfirstunseen + 1
            file.close()
          else
            lastplayed = 0 -- file doesn't exits
          end
        end
      end
    elseif(count== 6) then
      print(count.. " new mail notification")
      if firstunseen ~= nil then
        gpio.write(ledred, gpio.HIGH)
        if ((oldlastplayed ~= firstunseen) or (oldnewcount ~= newcount)) and letsplay == true then -- play the notification just once for each first UNSEEN
          play() -- PCM thing again, comment if no PCM used
          lastplayed = firstunseen
        end
      else
        gpio.write(ledred, gpio.LOW)
      end
    elseif(count == 7) then
      print(count.. " VIP mail notification")
      if vipcheck == true and vipmail ~= nil then
        gpio.write(ledblue, gpio.LOW)
        print ("  you have new mail from " ..vip_sender.. " :-)")
      else
        gpio.write(ledblue, gpio.HIGH)
      end
      if letsplay == true and firstunseen ~= nil and lastplayed == firstunseen then
      if file.open("unread_imaps.uns", "w+") then
        file.write(exists.. "," ..firstunseen..  "," ..lastplayed)
        file.close()
      end
     end
--    elseif(count == 8) then
--      imap_socket:send("a"..atcount.." LOGOUT\r\n")
--      imap_socket:on("receive",atdisplay)
--      print(count.. " LOGOUT")
--      atcount = atcount + 1
--      imap_socket:close()   
    else
      print(count.. " Loop\n")
      gpio.write(ledred, gpio.LOW)
      gpio.write(ledgreen, gpio.LOW)
      gpio.write(ledwhite, gpio.LOW)
      gpio.write(ledblue, gpio.HIGH)
      if letsplay == true and drv ~= nil then
        drv:stop() ----PCM thing, release PCM driver, comment if no PCM used
      end
      count = 2 -- skip LOGIN and (useless) LIST
      vipmail = nil
--      tmr.stop(3)
    end
  end
end

function connected(imap_socket)
  tmr.alarm(3, 15000, tmr.ALARM_AUTO, imapcheck) -- time should be longer than pcm notification file
end

gpio.write(ledred, gpio.LOW)
gpio.write(ledgreen, gpio.LOW)
gpio.write(ledwhite, gpio.LOW)
gpio.write(ledblue, gpio.HIGH) --blue LED is inverted on my ESP, change for normal logic

wifi.setmode(wifi.STATION)
wifi.sta.getap(function(t)
  if t then
    for k,v in pairs(t) do
      aplist(k)
    end
  else
--do nothing
  end
end)

tmr.alarm(1, 4000, tmr.ALARM_AUTO, function()
  if SSID == nil then
    print("Waiting for APs")
  else
  tmr.stop(1)
  print ("Connecting to "..SSID)
  wifi.sta.config(SSID,SSID_PASSWORD)
  end
end)

tmr.alarm(2, 5000, tmr.ALARM_AUTO, function()
  if wifi.sta.getip()== nil then
    print("IP unavaiable, waiting...")
  else
    tmr.stop(2)
    print("Config done, IP is "..wifi.sta.getip())
    print("Connect to server "..IMAP_SERVER.." on port "..IMAP_PORT)
    imap_socket = net.createConnection(net.TCP,1)  --connect to IMAP/IMAPS server, 0=IMAP, 1=IMAPS)
    imap_socket:on("connection",connected)
    flash(ledwhite,500)
    imap_socket:on("receive",atdisplay)  -- to check if server supports IMAP, if you want to skip this check, comment this line and uncomment 'response_processed = true' 2 lines below
    imap_socket:connect(IMAP_PORT,IMAP_SERVER)
--    response_processed = true  -- fake response if no IMAP support on server is checked
  end
end)