Minggu, 04 Desember 2016

Tutorial mengirim email beserta attachment pada NodeMCU Lua melalui SMTP provider anda

Tulisan ini membahas tentang pengiriman email melalui SMTP yang disediakan oleh provider anda. Dalam hal ini saya menggunakan internet Bolt, sehingga SMTP saya adalah:

local SMTP_SERVER = "smtp.internux.co.id"
local SMTP_PORT = "25"

Karena koneksi SMTP adalah tidak terenkripsi, sehingga ketika melakukan koneksi adalah menggunakan unsecure net socket connection.

smtp_socket = net.createConnection(net.TCP,0) -- 0 adalah unsecured
 Secara lengkap koding yang digunakan adalah:
-- Oleh Hendra Soewarno (0119067305)
  local MY_EMAIL = "hendra-it@internux.co.id"
 -- The SMTP server and port of your email provider.
 -- If you don't know it google [my email provider] SMTP settings
 local SMTP_SERVER = "smtp.internux.co.id"
 local SMTP_PORT = "25"
 -- The account you want to send email to
 local mail_to = "***********@gmail.com"
 -- These are global variables. Don't change their values
 -- they will be changed in the functions below
 local email_subject = ""
 local email_body = ""
 local count = 0 -- will be used to determine next action
 local good_news = {"220", "250", "250", "250", "354", "250", "221"}

 local smtp_socket = nil -- will be used as socket to email server

  -- The display() function will be used to print the SMTP server's response
 function display(sck,response)
    print("Got a response: ")
    print(response)
    response_code = string.sub(response,1,3)
    --print(response_code)
    --print(good_news[count+1])
    if(response_code~=good_news[count+1])then
        print("Send email failed!")
        count = 5 --Quit
    end 
    do_next()
 end
 -- The do_next() function is used to send the SMTP commands to the SMTP server in the required sequence.
 -- I was going to use socket callbacks but the code would not run callbacks after the first 3.
 function do_next()
       if(count == 0)then
         local IP_ADDRESS = wifi.sta.getip()
         print ("Send my IP: " .. IP_ADDRESS)
         smtp_socket:send("HELO "..IP_ADDRESS.."\r\n")
       elseif(count==1) then
         smtp_socket:send("MAIL FROM:" .. MY_EMAIL .. "\r\n")
       elseif(count==2) then
         smtp_socket:send("RCPT TO:" .. mail_to .."\r\n")
       elseif(count==3) then
         smtp_socket:send("DATA\r\n")
       elseif(count==4) then
         fin=file.open("temp","r")

         -- read chunk by chunk from temp file to prevent 1460 hard limit
         local function send(localSocket)
            local msg_chunk=file.read(1024)
            if msg_chunk ~= nil then
               localSocket:send(string.gsub(msg_chunk,"\r\n.\r\n",""))
               print(msg_chunk)
            else
               smtp_socket:on("sent", function() end)
               localSocket:send("\r\n.\r\n") --end of message
            end
         end

         -- triggers the send() function again once the first chunk of data was sent
         smtp_socket:on("sent", send) -- trigger next chunck
         send(smtp_socket) -- send first chunck
      
       elseif(count==5) then
          tmr.stop(0)
          smtp_socket:send("QUIT\r\n")
       else
         smtp_socket:close()
         print("Disconnected.")
       end
       count = count + 1
 end
 -- The connectted() function is executed when the SMTP socket is connected to the SMTP server.
 -- This function will create a timer to call the do_next function which will send the SMTP commands
 -- in sequence, one by one, every 5000 seconds.
 -- You can change the time to be smaller if that works for you, I used 5000ms just because.
 function connected(sck)
   print("Connected - Starting...")
 end
 -- @name send_email
 -- @description Will initiated a socket connection to the SMTP server and trigger the connected() function
 -- @param subject The email's subject
 -- @param body The email's body
 function send_email(subject, body_plain, body_html, attachment)
    count = 0
    content = {}
    fin=file.open(attachment,"r")
    if fin then
       repeat
          local line=file.read(1024)
          if line then table.insert(content, line) end
       until not line     
       fin.close()     
    else
        print("File :" .. attachment .. " not found!")
        return 0
    end
  
    email_subject = subject
    -- put message at a temp file
    fout = file.open("temp","w")
    if fout then     
       fout:write("From: \"".. MY_EMAIL .."\"\r\n")
       fout:write("To: \"".. mail_to .. "\"\r\n")     
       fout:write("Subject: ".. email_subject .. "\r\n")
       fout:write("MIME-Version: 1.0\r\n")  
       fout:write("Content-Type: multipart/mixed; boundary=\"----=_MixPart_920403024\"\r\n\r\n")
       fout:write("------=_MixPart_920403024\r\n")
       fout:write("Content-Type: multipart/alternative; boundary=\"----=_AltPart_920403024\"\r\n\r\n")
       fout:write("------=_AltPart_920403024\r\n")
       fout:write("Content-type: text/plain; charset=iso-8859-1\r\n")
       fout:write("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
       fout:write(body_plain .. "\r\n")
       fout:write("------=_AltPart_920403024\r\n")
       fout:write("Content-type: text/html; charset=iso-8859-1\r\n")
       fout:write("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
       fout:write(body_html .. "\r\n")
       fout:write("------=_AltPart_920403024--\r\n")
       fout:write("------=_MixPart_920403024\r\n")
       fout:write("Content-Type: text/plain; name=" .. attachment .. "\r\n")
       fout:write("Content-Transfer-Encoding: 8bit\r\n")
       fout:write("Content-Disposition: attachment; filename=" .. attachment .. "\r\n\r\n")
       local i = 1
       while table.getn(content) > 0 do
           fout:write(table.remove(content,1))
       end
       fout:write("\r\n")
       fout:write("------=_MixPart_920403024--\r\n")
       fout:close()
    end
    print ("Open Connection")
    smtp_socket = net.createConnection(net.TCP,0)
    smtp_socket:on("connection",connected)
    smtp_socket:on("receive",display)
    smtp_socket:connect(SMTP_PORT,SMTP_SERVER)
 end
 -- Send an email
 print ("Sending started...")
 send_email("ESP8266-GMailSender with attachment","Please refer to attachment","<p>Please refer to attachment!</p>","init.lua")
 

Sabtu, 03 Desember 2016

Send multipart email with attachment using gmail smtp (NodeMCU Lua)

The following coding can be used to send a multipart email with an attachment via smtp.gmail.com. This code can be executed in firmware NodeMCU that has been customized to incorporate some of the following modules:
NodeMCU custom build by frightanic.com
    branch: master
    commit: ec265a6c21db22640795f190bdcb8a4f014cdced
    SSL: true
    modules: crypto,dht,file,gpio,http,net,node,rtctime,sntp,tmr,uart,wifi
 build     built on: 2016-12-02 13:05
 powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)
In order to use gmail smtp to send mail through nodemcu, then you need to switch the mode less secure apps.

  
Lua code for send multipart message with attachment.
-- Add Sent multipart message with attachment by Hendra Soewarno (0119067305)
 ---- Modification for GMAIL by Hendra Soewarno, for a better performance version
 ----- Using response code from GMAIL to determine next step, and not try
 ----- all the step when error response from GMAIL. The original version using
 ----- timer and counter to determine each request to server.
 ------- Modifications for GMAIL by Andreas "Andy" Reischle: www.AReResearch.net
 ------- See https://support.google.com/a/answer/176600?hl=de for details on smtp with gmail
 ------- Now that NodeMCU has working SSL support, we can also talk to email services that
 ------- require encryption. 
 ------- Caveat: I have not looked into the SSL implementation, but I suspect it is vulnerable
 ------- to man-in-the-middle attacks as the client doesn't check the server's certificate.
 ------- 20160415 ARe
 -------------Original Credits:
 -------------
 ------------ Working Example: https://www.youtube.com/watch?v=CcRbFIJ8aeU
 ------------ @description a basic SMTP email example. You must use an account which can provide unencrypted authenticated access.
 ------------ This example was tested with an AOL and Time Warner email accounts. GMail does not offer unecrypted authenticated access.
 ------------ To obtain your email's SMTP server and port simply Google it e.g. [my email domain] SMTP settings
 ------------ For example for timewarner you'll get to this page http://www.timewarnercable.com/en/support/faqs/faqs-internet/e-mailacco/incoming-outgoing-server-addresses.html
 ------------ To Learn more about SMTP email visit:
 ------------ SMTP Commands Reference - http://www.samlogic.net/articles/smtp-commands-reference.htm
 ------------ See "SMTP transport example" in this page http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
 ------------ @author Miguel
 --no longer required because it is part of the crypto module: require("base64")
 -- The email and password from the account you want to send emails from
 local MY_EMAIL = "***********@gmail.com"
 local EMAIL_PASSWORD = "e*********"
 -- The SMTP server and port of your email provider.
 -- If you don't know it google [my email provider] SMTP settings
 local SMTP_SERVER = "smtp.gmail.com"
 local SMTP_PORT = "465"
 -- The account you want to send email to
 local mail_to = "hendra-it@******.co.id"
 -- These are global variables. Don't change their values
 -- they will be changed in the functions below
 local email_subject = ""
 local email_body = ""
 local count = 0 -- will be used to determine next action
 local good_news = {"220", "250", "334", "334", "235", "250", "250", "354", "250", "221"}
 
 local smtp_socket = nil -- will be used as socket to email server

  -- The display() function will be used to print the SMTP server's response
 function display(sck,response)
    print("Got a response: ")
    print(response)
    response_code = string.sub(response,1,3)
    --print(response_code)
    --print(good_news[count+1])
    if(response_code~=good_news[count+1])then
        print("Send email failed!")
        count = 8 --Quit
    end  
    do_next()
 end
 -- The do_next() function is used to send the SMTP commands to the SMTP server in the required sequence.
 -- I was going to use socket callbacks but the code would not run callbacks after the first 3.
 function do_next()
       if(count == 0)then
         local IP_ADDRESS = wifi.sta.getip()
         print ("Send my IP: " .. IP_ADDRESS)
         smtp_socket:send("HELO "..IP_ADDRESS.."\r\n")
       elseif(count==1) then
         smtp_socket:send("AUTH LOGIN\r\n")
       elseif(count == 2) then
         smtp_socket:send(crypto.toBase64(MY_EMAIL).."\r\n")
       elseif(count == 3) then
         smtp_socket:send(crypto.toBase64(EMAIL_PASSWORD).."\r\n")
       elseif(count==4) then
         smtp_socket:send("MAIL FROM:<" .. MY_EMAIL .. ">\r\n")
       elseif(count==5) then
         smtp_socket:send("RCPT TO:<" .. mail_to ..">\r\n")
       elseif(count==6) then
         smtp_socket:send("DATA\r\n")
       elseif(count==7) then
         fin=file.open("temp","r")

         -- read chunk by chunk from temp file to prevent 1460 hard limit
         local function send(localSocket)
            local msg_chunk=file.read(1024)
            if msg_chunk ~= nil then
               localSocket:send(string.gsub(msg_chunk,"\r\n.\r\n",""))
               print(msg_chunk)
            else
               smtp_socket:on("sent", function() end)
               localSocket:send("\r\n.\r\n") --end of message
            end
         end

         -- triggers the send() function again once the first chunk of data was sent
         smtp_socket:on("sent", send) -- trigger next chunck
         send(smtp_socket) -- send first chunck
       
       elseif(count==8) then
          tmr.stop(0)
          smtp_socket:send("QUIT\r\n")
       else
         smtp_socket:close()
         print("Disconnected.")
       end
       count = count + 1
 end
 -- The connectted() function is executed when the SMTP socket is connected to the SMTP server.
 -- This function will create a timer to call the do_next function which will send the SMTP commands
 -- in sequence, one by one, every 5000 seconds. 
 -- You can change the time to be smaller if that works for you, I used 5000ms just because.
 function connected(sck)
   print("Connected - Starting...")
 end
 -- @name send_email
 -- @description Will initiated a socket connection to the SMTP server and trigger the connected() function
 -- @param subject The email's subject
 -- @param body The email's body
 function send_email(subject, body_plain, body_html, attachment)
    count = 0
    content = {}
    fin=file.open(attachment,"r")
    if fin then
       repeat
          local line=file.read(1024)
          if line then table.insert(content, line) end
       until not line      
       fin.close()      
    else
        print("File :" .. attachment .. " not found!")
        return 0
    end
   
    email_subject = subject
    -- put message at a temp file
    fout = file.open("temp","w")
    if fout then      
       fout:write("From: \"".. MY_EMAIL .."\"<"..MY_EMAIL..">\r\n")
       fout:write("To: \"".. mail_to .. "\"<".. mail_to..">\r\n")      
       fout:write("Subject: ".. email_subject .. "\r\n")
       fout:write("MIME-Version: 1.0\r\n")   
       fout:write("Content-Type: multipart/mixed; boundary=\"----=_MixPart_920403024\"\r\n\r\n")
       fout:write("------=_MixPart_920403024\r\n")
       fout:write("Content-Type: multipart/alternative; boundary=\"----=_AltPart_920403024\"\r\n\r\n")
       fout:write("------=_AltPart_920403024\r\n")
       fout:write("Content-type: text/plain; charset=iso-8859-1\r\n")
       fout:write("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
       fout:write(body_plain .. "\r\n")
       fout:write("------=_AltPart_920403024\r\n")
       fout:write("Content-type: text/html; charset=iso-8859-1\r\n")
       fout:write("Content-Transfer-Encoding: quoted-printable\r\n\r\n")
       fout:write(body_html .. "\r\n")
       fout:write("------=_AltPart_920403024--\r\n")
       fout:write("------=_MixPart_920403024\r\n")
       fout:write("Content-Type: text/plain; name=" .. attachment .. "\r\n")
       fout:write("Content-Transfer-Encoding: 8bit\r\n")
       fout:write("Content-Disposition: attachment; filename=" .. attachment .. "\r\n\r\n")
       local i = 1
       while table.getn(content) > 0 do
           fout:write(table.remove(content,1))
       end
       fout:write("\r\n")
       fout:write("------=_MixPart_920403024--\r\n")
       fout:close()
    end
    print ("Open Connection")
    smtp_socket = net.createConnection(net.TCP,1)
    smtp_socket:on("connection",connected)
    smtp_socket:on("receive",display)
    smtp_socket:connect(SMTP_PORT,SMTP_SERVER)
 end
 -- Send an email
 print ("Sending started...")
 send_email("ESP8266-GMailSender with attachment","Please refer to attachment","<p>Please refer to attachment!</p>","init.lua")

Sample of multipart message:

From: "hendra.soewarno@gmail.com"<hendra.soewarno@gmail.com>
To: "hendra-it@*****.co.id"<hendra-it@******.co.id>
Subject: ESP8266-GMailSender with attachment
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="----=_MixPart_920403024"

------=_MixPart_920403024
Content-Type: multipart/alternative; boundary="----=_AltPart_920403024"

------=_AltPart_920403024
Content-type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable

Please refer to attachment
------=_AltPart_920403024
Content-type: text/html; charset=iso-8859-1
Content-Transfer-Encoding: quoted-printable

<p>Please refer to attachment!</p>
------=_AltPart_920403024--
------=_MixPart_920403024
Content-Type: text/plain; name=init.lua
Content-Transfer-Encoding: 8bit
Content-Disposition: attachment; filename=init.lua

Attachment file content
------=_MixPart_920403024--
.

Jumat, 02 Desember 2016

Sending email through gmail smtp using less secure apps (NodeMCU Lua)

This paper was written by some references from the Internet related to sending email through gmail using STMP NodeMCU based devices. Sending mail via smtp gmail account requires security settings to the mode Less secure apps. 

 Picture 1. Less secure apps setting

To run this example, you need to customize the firmware NodeMCU Lua by including several modules as follows:

NodeMCU custom build by frightanic.com
    branch: master
    commit: ec265a6c21db22640795f190bdcb8a4f014cdced
    SSL: true
    modules: crypto,dht,file,gpio,http,net,node,rtctime,sntp,tmr,uart,wifi
 build     built on: 2016-12-02 13:05
 powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)
 
Working code:
-- Modification for GMAIL by Hendra Soewarno, for a better performance version
 --- Using response code from GMAIL to determine next step, and not try
 --- all the step when error response from GMAIL. The original version using
 --- timer and counter to determine each request to server.
 ----- Modifications for GMAIL by Andreas "Andy" Reischle: www.AReResearch.net
 ----- See https://support.google.com/a/answer/176600?hl=de for details on smtp with gmail
 ----- Now that NodeMCU has working SSL support, we can also talk to email services that
 ----- require encryption.
 ----- Caveat: I have not looked into the SSL implementation, but I suspect it is vulnerable
 ----- to man-in-the-middle attacks as the client doesn't check the server's certificate.
 ----- 20160415 ARe
 -----------Original Credits:
 -----------
 ---------- Working Example: https://www.youtube.com/watch?v=CcRbFIJ8aeU
 ---------- @description a basic SMTP email example. You must use an account which can provide unencrypted authenticated access.
 ---------- This example was tested with an AOL and Time Warner email accounts. GMail does not offer unecrypted authenticated access.
 ---------- To obtain your email's SMTP server and port simply Google it e.g. [my email domain] SMTP settings
 ---------- For example for timewarner you'll get to this page http://www.timewarnercable.com/en/support/faqs/faqs-internet/e-mailacco/incoming-outgoing-server-addresses.html
 ---------- To Learn more about SMTP email visit:
 ---------- SMTP Commands Reference - http://www.samlogic.net/articles/smtp-commands-reference.htm
 ---------- See "SMTP transport example" in this page http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol
 ---------- @author Miguel
 --no longer required because it is part of the crypto module: require("base64")
 -- The email and password from the account you want to send emails from
 local MY_EMAIL = "************@gmail.com"
 local EMAIL_PASSWORD = "e*********"
 -- The SMTP server and port of your email provider.
 -- If you don't know it google [my email provider] SMTP settings
 local SMTP_SERVER = "smtp.gmail.com"
 local SMTP_PORT = "465"
 -- The account you want to send email to
 local mail_to = "hendra-it@*******.co.id"
 -- These are global variables. Don't change their values
 -- they will be changed in the functions below
 local email_subject = ""
 local email_body = ""
 local count = 0 -- will be used to determine next action
 local good_news = {"220", "250", "334", "334", "235", "250", "250", "354", "250", "221"}
 
 local smtp_socket = nil -- will be used as socket to email server

  -- The display() function will be used to print the SMTP server's response
 function display(sck,response)
    print("Got a response: ")
    print(response)
    response_code = string.sub(response,1,3)
    --print(response_code)
    --print(good_news[count+1])
    if(response_code~=good_news[count+1])then
        print("Send email failed!")
        count = 8 --Quit
    end 
    do_next()
 end
 -- The do_next() function is used to send the SMTP commands to the SMTP server in the required sequence.
 -- I was going to use socket callbacks but the code would not run callbacks after the first 3.
 function do_next()
       if(count == 0)then
         local IP_ADDRESS = wifi.sta.getip()
         print ("Send my IP: " .. IP_ADDRESS)
         smtp_socket:send("HELO "..IP_ADDRESS.."\r\n")
       elseif(count==1) then
         smtp_socket:send("AUTH LOGIN\r\n")
       elseif(count == 2) then
         smtp_socket:send(crypto.toBase64(MY_EMAIL).."\r\n")
       elseif(count == 3) then
         smtp_socket:send(crypto.toBase64(EMAIL_PASSWORD).."\r\n")
       elseif(count==4) then
         smtp_socket:send("MAIL FROM:<" .. MY_EMAIL .. ">\r\n")
       elseif(count==5) then
         smtp_socket:send("RCPT TO:<" .. mail_to ..">\r\n")
       elseif(count==6) then
         smtp_socket:send("DATA\r\n")
       elseif(count==7) then
         local message = string.gsub(
         "From: \"".. MY_EMAIL .."\"<"..MY_EMAIL..">\r\n" ..
         "To: \"".. mail_to .. "\"<".. mail_to..">\r\n"..
         "Subject: ".. email_subject .. "\r\n\r\n" ..
         email_body,"\r\n.\r\n","")
         smtp_socket:send(message.."\r\n.\r\n")
       elseif(count==8) then
          tmr.stop(0)
          smtp_socket:send("QUIT\r\n")
       else
         smtp_socket:close()
         print("Disconnected.")
       end
       count = count + 1
 end
 -- The connectted() function is executed when the SMTP socket is connected to the SMTP server.
 -- This function will create a timer to call the do_next function which will send the SMTP commands
 -- in sequence, one by one, every 5000 seconds.
 -- You can change the time to be smaller if that works for you, I used 5000ms just because.
 function connected(sck)
   print("Connected - Starting...")
 end
 -- @name send_email
 -- @description Will initiated a socket connection to the SMTP server and trigger the connected() function
 -- @param subject The email's subject
 -- @param body The email's body
 function send_email(subject,body)
    count = 0
    email_subject = subject
    email_body = body
    print ("Open Connection")
    smtp_socket = net.createConnection(net.TCP,1)
    smtp_socket:on("connection",connected)
    smtp_socket:on("receive",display)
    smtp_socket:connect(SMTP_PORT,SMTP_SERVER)
 end
 -- Send an email
 print ("Sending started...")
 send_email("ESP8266-GMailSender","Hi there from Hendra Soewarno!")
Scenario 1: Succeed result
Sending started...
Open Connection
Connected - Starting...
Got a response:
220 smtp.gmail.com ESMTP n17sm10345037pfg.80 - gsmtp

Send my IP: 172.21.12.106
Got a response:
250 smtp.gmail.com at your service

Got a response:
334 VXNlcm5hbWU6

Got a response:
334 UGFzc3dvcmQ6

Got a response:
235 2.7.0 Accepted

Got a response:
250 2.1.0 OK n17sm10345037pfg.80 - gsmtp

Got a response:
250 2.1.5 OK n17sm10345037pfg.80 - gsmtp

Got a response:
354  Go ahead n17sm10345037pfg.80 - gsmtp

Got a response:
250 2.0.0 OK 1480729924 n17sm10345037pfg.80 - gsmtp

Got a response:
221 2.0.0 closing connection n17sm10345037pfg.80 - gsmtp

Disconnected.

Scenario 2: Failed result cause by invalid username or password:
Sending started...
Open Connection
Connected - Starting...
Got a response:
220 smtp.gmail.com ESMTP y200sm10420052pfb.16 - gsmtp

Send my IP: 172.21.12.106
Got a response:
250 smtp.gmail.com at your service

Got a response:
334 VXNlcm5hbWU6

Got a response:
334 UGFzc3dvcmQ6

Got a response:
535-5.7.8 Username and Password not accepted. Learn more at
535 5.7.8  https://support.google.com/mail/?p=BadCredentials y200sm10420052pfb.16 - gsmtp

Got a response:
221 2.0.0 closing connection y200sm10420052pfb.16 - gsmtp

Disconnected.
Scenario 3:  Failed result cause by not set Less secure apps option to ON
Open Connection
Connected - Starting Timer
Got a response:
220 smtp.gmail.com ESMTP f132sm10155943pfa.72 - gsmtp

Send my IP: 172.21.12.106
Got a response:
250 smtp.gmail.com at your service

Got a response:
334 VXNlcm5hbWU6

Got a response:
334 UGFzc3dvcmQ6

Got a response:
534-5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbsC
534-5.7.14 tAI07EFTWQ94dXI6x41TyfjIFDp_NIYPEzYsY3XzS04a7E3blJJsFKjG73SKKcBX0dyrSX
534-5.7.14 2JHBhmdVqZJjGZVUMOmHoEKaLbxvktU8REWZrutoRNO-WU3a1VZ2r0jl49OWir6nt-stsf
534-5.7.14 zMidIGW5QGmbdlK3bnQRUflcL3XlCTL0M2MjM5os1xQJVeZuG3Fsd65l-zc9T364YtYMVk
534-5.7.14 2N1LRsEQoIYeXU4IxmNOmC_G-jzwk> Please log in via your web browser and
534-5.7.14 then try again.
534-5.7.14  Learn more at
534 5.7.14  https://support.google.com/mail/answer/78754 f132sm10155943pfa.72 - gsmtp
Got a response:
221 2.0.0 closing connection y200sm10420052pfb.16 - gsmtp

Disconnected.

Tutorial mengirim email pada NodeMCU Lua via smtp.gmail.com dengan fitur Less secure apps

Tulisan ini saya buat setelah melakukan browsing beberapa solusi pengiriman email melalui smtp.gmail.com, dan akhirnya menemukan kode berikut ini yang berhasil mengirim email, dengan catatan bahwa anda perlu menurunkan tingkat keamanan pada Google Account anda (Less secure apps) sebagai suatu fitur yang disediakan oleh Google untuk kompatibilitas dengan software client legacy yang dapat menyebabkan kelemahan pada account anda.

Sebelum anda mencoba koding berikut ini, maka anda perlu melakukan kustomisasi terhadap firmware NodeMCU dengan memasukan beberapa module sebagai berikut yang ditandai dengan warna merah.
NodeMCU custom build by frightanic.com
    branch: master
    commit: ec265a6c21db22640795f190bdcb8a4f014cdced
    SSL: true
    modules: crypto,dht,file,gpio,http,net,node,rtctime,sntp,tmr,uart,wifi
 build     built on: 2016-12-02 13:05
 powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)
Kemudian anda perlu menurunkan tingkat keamanan account Google anda (Less secure apps) yang dapat diakses melalui link.

Gambar 1. Mengaktifkan Less secure apps

Catatan:
Fitur Less secure apps disediakan oleh Google untuk memungkinkan akses ke GMail melalui aplikasi legacy yang berpotensi terhadap serangan Man in the Middle Attack, karena aplikasi klien tidak memeriksa sertifikat keamanan.

Adapun koding yang dapat digunakan untuk pengiriman email melalui stmp.gmail.com adalah sebagai berikut ini:
 -- Modifications for GMAIL by Andreas "Andy" Reischle: www.AReResearch.net 
 -- See https://support.google.com/a/answer/176600?hl=de for details on smtp with gmail 
 -- Now that NodeMCU has working SSL support, we can also talk to email services that 
 -- require encryption.  
 -- Caveat: I have not looked into the SSL implementation, but I suspect it is vulnerable 
 -- to man-in-the-middle attacks as the client doesn't check the server's certificate. 
 -- 20160415 ARe 
 --------Original Credits: 
 -------- 
 ------- Working Example: https://www.youtube.com/watch?v=CcRbFIJ8aeU 
 ------- @description a basic SMTP email example. You must use an account which can provide unencrypted authenticated access. 
 ------- This example was tested with an AOL and Time Warner email accounts. GMail does not offer unecrypted authenticated access. 
 ------- To obtain your email's SMTP server and port simply Google it e.g. [my email domain] SMTP settings 
 ------- For example for timewarner you'll get to this page http://www.timewarnercable.com/en/support/faqs/faqs-internet/e-mailacco/incoming-outgoing-server-addresses.html 
 ------- To Learn more about SMTP email visit: 
 ------- SMTP Commands Reference - http://www.samlogic.net/articles/smtp-commands-reference.htm 
 ------- See "SMTP transport example" in this page http://en.wikipedia.org/wiki/Simple_Mail_Transfer_Protocol 
 ------- @author Miguel 
 --no longer required because it is part of the crypto module: require("base64") 
 -- The email and password from the account you want to send emails from 
 local MY_EMAIL = "*******@gmail.com" 
 local EMAIL_PASSWORD = "e********" 
 -- The SMTP server and port of your email provider. 
 -- If you don't know it google [my email provider] SMTP settings 
 local SMTP_SERVER = "smtp.gmail.com" 
 local SMTP_PORT = "465" 
 -- The account you want to send email to 
 local mail_to = "hendra-**@*****.co.id" 
 -- These are global variables. Don't change their values 
 -- they will be changed in the functions below 
 local email_subject = "" 
 local email_body = "" 
 local count = 0 
 local smtp_socket = nil -- will be used as socket to email server 
 -- The display() function will be used to print the SMTP server's response 
 function display(sck,response) 
    print("Got a response: ") 
    print(response) 
 end 
 -- The do_next() function is used to send the SMTP commands to the SMTP server in the required sequence. 
 -- I was going to use socket callbacks but the code would not run callbacks after the first 3. 
 function do_next() 
       if(count == 0)then 

         local IP_ADDRESS = wifi.sta.getip() 
         print ("Send my IP: " .. IP_ADDRESS) 
         smtp_socket:send("HELO "..IP_ADDRESS.."\r\n") 
       elseif(count==1) then 

         smtp_socket:send("AUTH LOGIN\r\n") 
       elseif(count == 2) then 

         smtp_socket:send(crypto.toBase64(MY_EMAIL).."\r\n") 
       elseif(count == 3) then 

         smtp_socket:send(crypto.toBase64(EMAIL_PASSWORD).."\r\n") 
       elseif(count==4) then 

         smtp_socket:send("MAIL FROM:<" .. MY_EMAIL .. ">\r\n") 
       elseif(count==5) then 

         smtp_socket:send("RCPT TO:<" .. mail_to ..">\r\n") 
       elseif(count==6) then 

         smtp_socket:send("DATA\r\n") 
       elseif(count==7) then 

         local message = string.gsub( 
         "From: \"".. MY_EMAIL .."\"<"..MY_EMAIL..">\r\n" .. 
         "To: \"".. mail_to .. "\"<".. mail_to..">\r\n".. 
         "Subject: ".. email_subject .. "\r\n\r\n" .. 
         email_body,"\r\n.\r\n","") 
         smtp_socket:send(message.."\r\n.\r\n") 
       elseif(count==8) then 

          tmr.stop(0) 
          smtp_socket:send("QUIT\r\n") 
       else 
         smtp_socket:close() 
       end
       count=count+1

 end 
 -- The connectted() function is executed when the SMTP socket is connected to the SMTP server. 
 -- This function will create a timer to call the do_next function which will send the SMTP commands 
 -- in sequence, one by one, every 5000 seconds.  
 -- You can change the time to be smaller if that works for you, I used 5000ms just because. 
 function connected(sck) 
   print("Connected - Starting Timer") 
   tmr.alarm(0,5000,1,do_next) 
 end 
 -- @name send_email 
 -- @description Will initiated a socket connection to the SMTP server and trigger the connected() function 
 -- @param subject The email's subject 
 -- @param body The email's body 
 function send_email(subject,body) 
    count = 0 
    email_subject = subject 
    email_body = body 
    print ("Open Connection") 
    smtp_socket = net.createConnection(net.TCP,1) 
    smtp_socket:on("connection",connected) 
    smtp_socket:on("receive",display) 
    smtp_socket:connect(SMTP_PORT,SMTP_SERVER) 
 end 
 -- Send an email 
 print ("Sending started...") 
 send_email("ESP8266-GMailSender","Hi there!") 
Jika anda mencoba menjalankan aplikasi diatas, tanpa melakukan setting Less secure apps, maka Google akan mengirimkan email pemberitahuan bahwa adanya upaya Sign-in ke account anda dengan kondisi Less secure apps.

  Gambar 2. Pemberitahuan Upaya Sign-in Less secure apps

Maka anda dapat melakukan klik pada allowing access to less secure apps sesuai dengan kondisi yang disyaratkan oleh contoh diatas.

Are you the one who tried signing in?
Google will continue to block sign-in attempts from the app you're using because it has known security problems or is out of date. You can continue to use this app by allowing access to less secure apps, but this may leave your account vulnerable.

Jika setting Less secure app berhasil diaktifkan, maka akan dikirimkan email konfirmasi sebagai berikut:

 Gambar 3. Pemberitahuan pengaktifan Less secure apps

Hasil output aplikasi kalau email berhasil dikirim:

Open Connection
Connected - Starting Timer
Got a response:
220 smtp.gmail.com ESMTP t184sm10153815pgt.36 - gsmtp

Send my IP: 172.21.12.106
Got a response:
250 smtp.gmail.com at your service

Got a response:
334 VXNlcm5hbWU6

Got a response:
334 UGFzc3dvcmQ6

Got a response:
235 2.7.0 Accepted

Got a response:
250 2.1.0 OK t184sm10153815pgt.36 - gsmtp

Got a response:
250 2.1.5 OK t184sm10153815pgt.36 - gsmtp

Got a response:
354  Go ahead t184sm10153815pgt.36 - gsmtp

Got a response:
250 2.0.0 OK 1480722979 t184sm10153815pgt.36 - gsmtp

Got a response:
221 2.0.0 closing connection t184sm10153815pgt.36 - gsmtp
Hasil Output kalau aplikasi dijalankan tetapi setting Less secure apps belum dilakukan:
Open Connection
Connected - Starting Timer
Got a response:
220 smtp.gmail.com ESMTP f132sm10155943pfa.72 - gsmtp

Send my IP: 172.21.12.106
Got a response:
250 smtp.gmail.com at your service

Got a response:
334 VXNlcm5hbWU6

Got a response:
334 UGFzc3dvcmQ6

Got a response:
534-5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbsC
534-5.7.14 tAI07EFTWQ94dXI6x41TyfjIFDp_NIYPEzYsY3XzS04a7E3blJJsFKjG73SKKcBX0dyrSX
534-5.7.14 2JHBhmdVqZJjGZVUMOmHoEKaLbxvktU8REWZrutoRNO-WU3a1VZ2r0jl49OWir6nt-stsf
534-5.7.14 zMidIGW5QGmbdlK3bnQRUflcL3XlCTL0M2MjM5os1xQJVeZuG3Fsd65l-zc9T364YtYMVk
534-5.7.14 2N1LRsEQoIYeXU4IxmNOmC_G-jzwk> Please log in via your web browser and
534-5.7.14 then try again.
534-5.7.14  Learn more at
534 5.7.14  https://support.google.com/mail/answer/78754 f132sm10155943pfa.72 - gsmtp

Got a response:
530-5.5.1 Authentication Required. Learn more at
530 5.5.1  https://support.google.com/mail/?p=WantAuthError f132sm10155943pfa.72 - gsmtp

Got a response:
530-5.5.1 Authentication Required. Learn more at
530 5.5.1  https://support.google.com/mail/?p=WantAuthError f132sm10155943pfa.72 - gsmtp

Got a response:
530-5.5.1 Authentication Required. Learn more at
530 5.5.1  https://support.google.com/mail/?p=WantAuthError f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
221 2.0.0 closing connection f132sm10155943pfa.72 - gsmtp
 Hasil Output kalau aplikasi dijalankan tetapi username atau password salah:
Open Connection
Connected - Starting Timer
Got a response:
220 smtp.gmail.com ESMTP f132sm10155943pfa.72 - gsmtp

Send my IP: 172.21.12.106
Got a response:
250 smtp.gmail.com at your service

Got a response:
334 VXNlcm5hbWU6

Got a response:
334 UGFzc3dvcmQ6

Got a response:
534-5.7.14 <https://accounts.google.com/signin/continue?sarp=1&scc=1&plt=AKgnsbsC
534-5.7.14 tAI07EFTWQ94dXI6x41TyfjIFDp_NIYPEzYsY3XzS04a7E3blJJsFKjG73SKKcBX0dyrSX
534-5.7.14 2JHBhmdVqZJjGZVUMOmHoEKaLbxvktU8REWZrutoRNO-WU3a1VZ2r0jl49OWir6nt-stsf
534-5.7.14 zMidIGW5QGmbdlK3bnQRUflcL3XlCTL0M2MjM5os1xQJVeZuG3Fsd65l-zc9T364YtYMVk
534-5.7.14 2N1LRsEQoIYeXU4IxmNOmC_G-jzwk> Please log in via your web browser and
534-5.7.14 then try again.
534-5.7.14  Learn more at
534 5.7.14  https://support.google.com/mail/answer/78754 f132sm10155943pfa.72 - gsmtp

Got a response:
530-5.5.1 Authentication Required. Learn more at
530 5.5.1  https://support.google.com/mail/?p=WantAuthError f132sm10155943pfa.72 - gsmtp

Got a response:
530-5.5.1 Authentication Required. Learn more at
530 5.5.1  https://support.google.com/mail/?p=WantAuthError f132sm10155943pfa.72 - gsmtp

Got a response:
530-5.5.1 Authentication Required. Learn more at
530 5.5.1  https://support.google.com/mail/?p=WantAuthError f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
502 5.5.1 Unrecognized command. f132sm10155943pfa.72 - gsmtp

Got a response:
221 2.0.0 closing connection f132sm10155943pfa.72 - gsmtp

Jumat, 25 November 2016

Tutorial mengendalikan Led-Builtin pada WeMos NodeMCU behind the Firewall dengan Android melalui Firebase Realtime Database

Tulisan ini adalah kelanjutan dari tulisan sebelumnya tentang bagaimana mengakses board IoT yang berada dibelakang firewall yang tidak memperbolehkan port forwarding.

Agar board IoT dapat dikendalikan dari luar melalui Android, padahal IoT board berada dibelakang firewall, berarti bahwa diperlukan suatu perantara diantara kedua perangkat. Dalam hal ini saya akan menggunakan Firebase Realtime Database sebagai sarana perantara tersebut.

Gambar 1. Firebase Database sebagai perantara

Dalam hal ini IoT board akan mengambil nilai request dari database Firebase, kemudian menentukan status led builtin berdasarkan nilai request (on/off), dan selanjutnya akan mengupdate nilai response sebagai konfirmasi bahwa request telah berhasil dilakukan.

startup = true
request = false
position = "off"

database = "https://friendlychat-6ae71.firebaseio.com/"
nodeUpdate = "ledbuiltin.json"
dbSecret = "kia43oq8Nn76kTJQhLIshyRpSYDHoEwjGTI*****"
urlUpdate = database .. nodeUpdate .. "?auth=" .. dbSecret

-- initial payload
payload = "{\"request\":\"".. position .. "\",\"response\":\"".. position .. "\"}"

nodeRead = "ledbuiltin/request.json"
urlRead = database .. nodeRead .. "?auth=" .. dbSecret

function updateToFirebase()

   if not request then --sentinel
      request = true 

      http.request(urlUpdate,
         "PATCH",
         "Content-Type: application/json\r\n",
         payload,
         function(code, data)
            if (code ~= 200) then
               print("Failed : HTTP error code : " .. code)
               request = false
            else
               -- stop alarm, mission succeed
               tmr.stop(0)
               print("Succeed : " .. code, data)
               startup = false
               request = false
            end
         end)
    end
end

function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function readfromFirebase()

   if not startup then -- sentinel
      if not request then -- sentinel
         request = true
         http.request(urlRead,
            "GET",
            "Content-Type: application/json\r\n",
            nil,
            function(code, data)
               if (code ~= 200) then
                  print("Failed : HTTP error code : " .. code)
                  request = false
               else
                  print("Succeed : " .. code, data)
                  slash = "\\"                   
                  newposition = string.gsub(string.gsub(data,slash,""),"\"","")
                  if position ~= newposition then
                     position = newposition
                     --update response payload
                     payload = "{\"response\":\"".. position .. "\"}"
                     led_builtin(position)
                     tmr.alarm(0,2000,1,updateToFirebase) -- dua detik berikutnya update status
                  end           
                  request = false   
               end
            end)
       end      
    end
end

tmr.alarm(0,2000,1,updateToFirebase)
tmr.alarm(1,4000,1,readfromFirebase) -- setiap 4 detik
Pengembangan aplikasi pada sisi Android akan menggunakan AppInventor yang berfungsi sebagai IDE yang bersifat Cloud yang memungkinkan anda melakukan pengembangan aplikasi Android tanpa menggunakan ADT.

 Gambar 2. Design Screen dan Component pada Aplikasi

Gambar 3. Block Code pada Aplikasi

Selasa, 22 November 2016

Tutorial mengendalikan Led-Builtin pada WeMos NodeMCU behind the Firewall melalui Firebase Realtime Database

Tulisan ini saya buat setelah menyadari bahwa provider saya tidak menyediakan ip public pada interface luar modem saya, sehingga saya tidak dapat mengendalikan iot board saya melalui internet (selama ini uji coba dilakukan secara intranet)

Jika anda memiliki ip public, maka koneksi ke iot board anda dapat dilakukan dengan mudah yaitu dengan mengaktifkan fitur ip forwarding pada modem anda, sehingga koneksi pada port tertentu pada interface external modem anda (melalui ip public) dapat diforward ke host intranet anda pada port tertentu (default gateway dari host intranet yang menjadi target diset ke ip interface dalam modem anda)

Tetapi kondisi yang kita hadapi saat ini adalah provider tidak menyediakan ip public, sehingga kita tidak dapat mencapai iot board dari internet. Setelah melakukan pemikiran akhirnya timbul ide untuk bagaimana kita dapat mengendalikan led-builtin pada board WeMos yang berada dibelakang firewall?

Pada tulisan ini saya mencoba menggunakan firebase realtime database untuk menyimpan request ON/OFF terhadap led-builtin, secara berkala board WeMos akan melakukan query ke Firebase untuk mengambil nilai dari tag tertentu.

Langkah-langkah:
1. Aktifkan ke firebird console anda, dan dapatkan token database Secret anda.
2. Lakukan koding berikut:

startup = true
request = false
position = "off"

-- ganti dengan url database anda
database = "https://friendlychat-6ae71.firebaseio.com/"
nodeUpdate = "ledbuiltin.json"
-- ganti dengan database secret anda
dbSecret = "kia43oq8Nn76kTJQhLIshyRpSYDHoEwjGTI*****"
urlUpdate = database .. nodeUpdate .. "?auth=" .. dbSecret

-- initial payload
payload = "{\"request\":\"".. position .. "\",\"response\":\"".. position .. "\"}"

nodeRead = "ledbuiltin/request.json"
urlRead = database .. nodeRead .. "?auth=" .. dbSecret

function updateToFirebase()

   if not request then --sentinel
      request = true

      http.request(urlUpdate,
         "PATCH",
         "Content-Type: application/json\r\n",
         payload,
         function(code, data)
            if (code ~= 200) then
               print("Failed : HTTP error code : " .. code)
               request = false
            else
               -- stop alarm, mission succeed
               tmr.stop(0)
               print("Succeed : " .. code, data)
               startup = false
               request = false
            end
         end)
    end
end

function led_builtin(position)
    gpio.mode(4, gpio.OUTPUT)
    if (position == "on") then
        gpio.write(4, gpio.LOW) -- LED BUILTIN ON
    else
        gpio.write(4, gpio.HIGH) -- LED BUILTIN ON
    end
end

function readfromFirebase()

   if not startup then -- sentinel
      if not request then -- sentinel
         request = true
         http.request(urlRead,
            "GET",
            "Content-Type: application/json\r\n",
            nil,
            function(code, data)
               if (code ~= 200) then
                  print("Failed : HTTP error code : " .. code)
                  request = false
               else
                  print("Succeed : " .. code, data)
                  newposition = string.gsub(data,"\"","")
                  if position ~= newposition then
                     position = newposition
                     --update response payload
                     payload = "{\"response\":\"".. position .. "\"}"
                     led_builtin(position)
                     tmr.alarm(0,2000,1,updateToFirebase) -- dua detik berikutnya update status
                  end          
                  request = false  
               end
            end)
       end     
    end
end

tmr.alarm(0,2000,1,updateToFirebase)
tmr.alarm(1,4000,1,readfromFirebase) -- setiap 4 detik
2.Setelah script diatas dijalankan, maka pada database Firebase anda  terbentuk node baru dengan nama ledbuiltin yang memiliki dua tag yaitu request dan response yang masing-masing bernilai awal "off"


Gambar 1. Hasil initialisasi nilai awal

3. Ubah nilai tag request dari "off" menjadi "on"

Gambar 2. Hasil perubahan nilai request menjadi "off"

4. Led builtin pada board akan hidup, dan tag response akan diset menjadi "on"

Gambar 3. Hasil respon dari board.

Adapun penjelasan pada koding diatas adalah pada awalnya akan diinitialisasi nilai awal untuk node ledbuiltin pada Firebase dengan tag request dan response ke "off", selanjutnya secara berkala (4 detik sekali) dan dilakukan pemantauan terhadap nilai request dengan menggunakan metode REST yang disediakan oleh firebase. Jika nilai request berbeda dengan nilai pembacaan sebelumnya, maka akan dilanjutkan dengan mengatur led-builtin pada board sesuai dengan nilai pembacaan terbaru. Setelah status led-builtin disesuaikan, maka selanjutnya dalah mengupdate status respon sesuai dengan status led-builtin.

Keyword:  behind the firewall, without using public ip address, and not using port forwarding

Senin, 21 November 2016

Tutorial memantau suhu dan kelembaban via Android dengan memanfaatkan WeMos (NodeMCU Lua) + Firebase Realtime Database + App Inventor

Pada tulisan sebelumnya telah kita bahas bagaimana push data dari WeMos ke Firebase Realtime Database dengan menggunakan bahasa pemrograman Lua (anda membutuhkan firmware yang dikustomisasi dengan module dht, http, sntp, rtctimer dan SSL).
NodeMCU custom build by frightanic.com
    branch: master
    commit: 7b83bbb2ea134cd85ac9d63108603cc02c4e20f7
    SSL: true
    modules: dht,file,gpio,http,i2c,net,node,rtctime,sntp,tmr,uart,wifi
 build     built on: 2016-11-15 12:39
 powered by Lua 5.1.4 on SDK 1.5.4.1(39cb9a32)

Module sntp diperlukan untuk sinkronisasi waktu antara WeMos dengan time server melalui NTP dan diupdate ke software RTC yang terdapat pada NodeMCU, module http dan SSL diperlukan untuk melakukan push data ke Firebase yang membutuhkan koneksi https)


Gambar 1. Hasil Push ke Firebase Realtime Database

Untuk melakukan push data ke Firebase Realtime Database silakan mengacu pada tulisan saya sebelumnya.

Pada tulisan ini kita akan memantau data yang dipush ke Firebase dengan menggunakan Android. Untuk memudahkan proses developement, pada tulisan ini kita akan menggunakan App Inventory. Adapun keunggulan dari App Inventor dibandingkan dan Android Studio adalah anda tidak perlu menginstalasi software ADT apapun, cukup memiliki web browser, App Inventor adalah IDE yang ditempatkan di Cloud. Untuk mengaktifkan App Inventor, silakan mengacu pada alamat ini http://appinventor.mit.edu/explore/

Langkah-langkah:
1. Klik pada Create Apps
2. Login dengan Google Account anda
3. Klik pada Project, dan pilih New Project dan isikan FirebaseIOT sebagai nama project.
4. Tambahkan sebuah TextBox dan FirebaseDB komponen ke Screen1

Gambar 2. Komponen TextBox dan FireBaseDB pada Screen1

5. Set properti pada komponen FirebaseDB1 sebagai berikut:


Gambar 3. Setting pada Komponen FirebaseDB1

FirebaseToken merupakan Database Secret project Firebase Anda, yang dapat anda peroleh dari dari Console Firebase (Klik pada Project Setting, pilih tab Account Service, klik pada Database Secret, dan klik pada Show, copy dan paste ke FirebaseToken)

FirebaseURL merupakan URL dari database anda, yang dapat diperoleh dengan klik pada database.


 Gambar 4. FirebaseURL

ProjectBucket diisi node database anda yang dalam hal ini adalah dht11.

6. Buatlah koding untuk Screen1 pada jendela Blocks.

Gambar 5. Koding pada Aplikasi

Block koding diatas dapat dijelaskan bahwa event when FirebaseDB1.DataChanged akan dipicu setiap kali adanya perubahan data pada database anda pada node dht11 sebagai akibat push data dari NodeMCU silakan mengacu pada tulisan saya sebelumnya.

 7. Klik pada Build, dan pilih App (Provide QR code for .apk)

 Gambar 5. Build Aplikasi

8. Scan QRcode untuk download dan install aplikasi, anda perlu mengaktifkan keamanan untuk memperbolehkan instalasi dari sumber yang tidak diketahui pada menu setting perangkat anda.


 Gambar 6. QR Code Aplikasi

7. Berikut ini adalah hasil run aplikasi.

Gambar 7. Hasil Run Aplikasi.