Git Product home page Git Product logo

shawk's Introduction

Shawk Logo Shawk - Free SMS with Python using SMTP and IMAP

A Python smtplib and imapclient wrapper to send and receive SMS messages through SMS gateways with a Gmail login. Perfect for Internet of Things projects that use a Raspberry Pi to text you.

Disclaimer

This project is a work in progress, and as such, the API may change drastically before version 1.0 is reached. This repo may be out-of-sync with the PyPi version, so if you're interested in using the bleeding-edge features added since last tag, clone the repo and import locally. Tags will reflect PyPi published versions.

Installation

Shawk is available on PyPi as such:

pip install -U shawk

Documentation

To see full documentation and a more detailed Getting Started page, see here.

Contributing

I welcome all sorts of contributions - code, docs, tests, bugs, etc. If you'd like to contribute code, please make sure your tests continue to pass after making your changes. Additionally, if your new code requires any testing, please write your tests in the appropriate file.

Testing

Tests can be run with Pytest.

  1. Run pip install -U pytest to install pytest
  2. Locally install this Shawk package with pip install -e .
  3. Then simply run pytest in the root directory to execute tests

Simple Usage Example

Sending Messages

Create a Client:

Simply import shawk and define a Client by providing a Gmail username and password.

import shawk
client = shawk.Client('[email protected]', 'password')

(Optionally) Add Contacts:

Use the add_contact() function to add a contact's number as string or integer, carrier, and (optionally) a name.

some_contact = client.add_contact("5551234567", 'Carrier', 'Name')

Send SMS:

Shawk can send texts either by providing a Contact object, phone number, or name.

client.send("Message content to send", some_contact) # or contact=some_contact
client.send("Message content to send", name="Name")
client.send("Message content to send", number="5551234567")

Receiving Messages Automatically

Shawk clients can be configured to automatically refresh their inbox and report back with new messages. In this mode, Shawk will poll the IMAP server periodically and check for new messages.

To do this, create your client with the auto=True parameter, or see the docs for other methods.

client = Shawk.Client('[email protected]', 'password')
client.setup_inbox('password', auto=True)

You can also change how frequently the server is queried:

client.set_refresh_interval(30) # Period time in seconds

Each time Shawk encounters a new message, it will pass its Message object to the handler function. You can define new behaviors by creating functions with the @client.text_handler(regex, flags) or @client.contact_handler(contact) decorators.

This allows you to define certain behaviors based on who texted you or what was in the message without complicating your actual behavior logic.

For a small example:

@client.text_handler()
def handler(client, msg):
    print("Hey, we're popular! {} texted us!".format(msg.sender))
    client.send("Hello, world!", msg.sender)

You can define more complicated text_handlers and contact_handlers, but we'll save that for the docs.

shawk's People

Contributors

hawkins avatar

Stargazers

 avatar Wil Buchanan avatar  avatar Phillip Peng avatar John Gotti Sr. avatar  avatar  avatar Jimmy ร…. avatar Zevan Rosser avatar Mike Pennington avatar Jeffrey Terry avatar  avatar Jesse Tatum avatar Hunter Holder avatar Konstantin L. Golovko avatar

Watchers

James Cloos avatar Jaime A. Mendez avatar  avatar  avatar

shawk's Issues

Build test suite

This one's been a long time coming...

Until now, I've been painstakingly and manually testing each and every feature with a test.py file I've gitignored. Mostly because testing requires a Gmail account and a phone number to test with. So I'd write a quick Shawk script, add some behaviors, and test whatever feature or bug I needed to with my own Gmail and phone.

I'd like to build a test suite that incorporates an email 'spoofer' in a sense that it...

  • does not require a real phone
  • preferably does not require a Gmail account

The first being a requirement, and the second being a nicety.

What I can see we need is a means to spoof an email address like "[email protected]" to spoof a Verizon address, and an IMAP server to spoof. The IMAP server spoof may be unnecessary, since we can limit its impact on the outside world. For instance, ideally, we could prevent replying to spoofed numbers somehow - so maybe our __sendmail() function is just disabled entirely. We know it works, so we can instead wire this test Shawk instance up to print the input passed to __sendmail() to determine what would happen. That way we can test IMAP features like moving folders, but can't impact the outside world beyond that.

Additionally, we could configure the IMAP server to be read only. Not sure that's necessary or useful, though.

Would like to incorporate pytest for this effort.

Add IMAP support for replying to texts

Adding IMAP support (as demonstrated here) will enable Shawk to watch for replies or incoming texts. As this means Shawk will be able to read the email of whoever uses it, some steps must be taken to manually enable this feature and define how Shawk is allowed to search the inbox to ensure privacy.

Include emoji support?

Being able to respond with emojis would really help Shawk catch up to modern texting.

Using carpedm20's emoji available on pip should make this pretty easy, and inuitive to those used to typing similar emojis in applications like Slack.

Worth noting that a raw parameter should be added to support raw message contents. That is to say, raw messages would not be emojized.

SSLError occurs unexpectedly when refreshing inbox with ssl=True

The following error is encountered when refreshing inbox with ssl=True:
SSLError: [('SSL routines', 'SSL3_GET_RECORD', 'decryption failed or bad record mac')]

This error occurs when using imapclient==1.0.2 with ssl=True. It seems that this does not occur when using ssl=False on some networks, yet on some other networks, setting ssl=False will cause the IMAP server to be unable to connect.

I've tried using imapclient==0.13 to avoid an SSL Version error, but this seems to be the new issue.

Not sure what's going on here, but may have something to do with this SO post on multiprocessing in Python with SSL.

Full stack trace is as follows:

Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 801, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 1073, in run
    self.function(*self.args, **self.kwargs)
  File "/home/chrx/git/shawk/shawk/Client.py", line 197, in auto_refresh
    self.refresh()
  File "/home/chrx/git/shawk/shawk/Client.py", line 207, in refresh
    raw_msgs = self.imap.fetch(uids, ['BODY[TEXT]', 'BODY[HEADER.FIELDS (FROM)]', 'INTERNALDATE'])
  File "/usr/local/lib/python2.7/dist-packages/imapclient/imapclient.py", line 972, in fetch
    typ, data = self._imap._command_complete('FETCH', tag)
  File "/usr/lib/python2.7/imaplib.py", line 910, in _command_complete
    typ, data = self._get_tagged_response(tag)
  File "/usr/lib/python2.7/imaplib.py", line 1017, in _get_tagged_response
    self._get_response()
  File "/usr/lib/python2.7/imaplib.py", line 974, in _get_response
    data = self.read(size)
  File "/usr/local/lib/python2.7/dist-packages/imapclient/tls.py", line 181, in read
    return self.file.read(size)
  File "/usr/local/lib/python2.7/dist-packages/backports/ssl/core.py", line 488, in read
    data = _safe_ssl_call(False, self._sock, 'recv', maxbufsize)
  File "/usr/local/lib/python2.7/dist-packages/backports/ssl/core.py", line 222, in _safe_ssl_call
    raise SSLError(*e.args)
SSLError: [('SSL routines', 'SSL3_GET_RECORD', 'decryption failed or bad record mac')]

Find messages in inbox by matching a gateway

Currently messages are determined to be 'text messages' only by their short length.

Ideally refreshInbox() will look at each message in the inbox and determine if their sender matches the pattern outlined by any gateway defined in Contact.py's list of gateways.

Improve Workflow

Using Shawk in its current state just feels a little bit wonky, given it does not have a clear workflow and features change from version to version.

Ideally a workflow would be more obvious. Potential ideas:

  • Setup Inbox on initialization unless some argument specifies not to
  • Refresh inbox automatically (either on some configurable interval or whenever inbox is requested)
  • Handler callback function to treat messages
  • Function chaining

Function Chaining

Function chaining may streamline the workflow significantly. Something like this...

client = shawk.Client(email, password)
client.addContact(number, carrier, name).text('Send this message to the contact')

for text in client.inbox:
    text.sender.text('Respond').removeContact()

[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1045)

My code:
import shawk client = shawk.Client('******@gmail.com', '************')

My error:

Traceback (most recent call last): File "/", line 2, in <module> client = shawk.Client('******@gmail.com', '************') File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/shawk/Client.py", line 49, in __init__ self.setup_inbox(pwd, auto=self.auto_refresh_enabled) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/shawk/Client.py", line 163, in setup_inbox self.imap = imapclient.IMAPClient('imap.gmail.com', ssl=ssl) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/imapclient/imapclient.py", line 254, in __init__ self._imap = self._create_IMAP4() File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/imapclient/imapclient.py", line 289, in _create_IMAP4 self._timeout) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/imapclient/tls.py", line 44, in __init__ imaplib.IMAP4.__init__(self, host, port) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/imaplib.py", line 197, in __init__ self.open(host, port) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/imapclient/tls.py", line 50, in open self.sock = wrap_socket(sock, self.ssl_context, host) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/site-packages/imapclient/tls.py", line 32, in wrap_socket return ssl_context.wrap_socket(sock, server_hostname=host) File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/ssl.py", line 412, in wrap_socket session=session File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/ssl.py", line 850, in _create self.do_handshake() File "/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7/ssl.py", line 1108, in do_handshake self._sslobj.do_handshake() ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1045)

Move away from Gmail in favor of configurability with preconfigurable settings

Gmail is great - but it has its limits. It is possible that other email services have much looser limits on sending messages via SMTP.

That being said, "everyone" has a Gmail account, so I'd like to make it an opt-in configuration.

Maybe shawk.Client should have a system for configuring SMTP similar to IMAP already does. However, I would like to add a cookie-cutter configuration function such as client.preconfigure_smtp_settings('Gmail') that configures client.smtp as it already does. Similarly, client.preconfigure_imap_settings('Gmail') would set the IMAP host to 'imap.gmail.com' like Shawk assumes right now.

I'd like to support these platforms for preconfigure settings

  • Gmail.com
  • Outlook.com
  • Yahoo.com
  • And any other major providers, but these are the 3 I see the most in the wild.

Shawk Native mobile client

Currently Shawk can only send messages via an email address, Gmail in particular.

It would be wonderful if a "Shawk Native" client existed such that a regular Python Shawk Client could connect to a Shawk Native client to send and receive messages using that phone's number (instead of / in addition to SMTP & IMAP).

All this Native client would need to do is:

  • Run service in background
  • Obtain device permissions for SMS
  • Potentially start service on device startup without need to open app
  • Potentially obtain device permissions for Contacts to forward information to Shawk Client as well
  • Potentially limit number of Shawk Clients allowed to communicate with it (1 seems fine)

The native client would likely just forward every received message to the Shawk client for processing and listen for messages from Shawk to reroute to actual phone contacts.

Potential implementations

While not a permanent solution, I've managed to use Shawk via Termux on an Android phone before. Maybe a shawk.Native class could be defined that could be configured at runtime to utilize native device features like true SMS and contacts? Again, this is not as native as I would like, but it may be a possibility, if only temporarily.

More seriously, Kivy could be key. I've never used it before, but for something as simple as forwarding text messages and opening a socket to communicate with Shawk, it should be simple enough. My biggest concern there is allowing the app to run in background, but this great blog post shows otherwise.

Target Platforms

I would target Android initially, since I have a small number of Android devices with which to test the app. Kivy supports iOS, but from what I understand, distributing third party apps (since I have no intention of publishing this on Apple's app store) is quite a chore. Ideally I would support iOS as well, but if it turns out to be such a pain point, I may have to wait until a collaborator shows up with an iPhone to help develop and test with.

Organization

Should Shawk stay as a monorepo and house Shawk in one folder and Shawk Native in another, or should it be split to two repos? I tend to think keeping Native in this folder would be fine, since Native won't aim to do much other than provide a middleware for Shawk to interface with and thus won't be updated much, let alone independently of Shawk.

Add ability to load contacts from file(s)

Right now, the easiest way to load a large number of contacts is to pass arrays of numbers, carriers, and names. This is inconvenient at best, so it would be nice if users could load some format of files that represent contacts. Maybe something like...

# Number, Carrier, Name
# Comments with # symbol
5551234567, Carrier, Name
5551234568, Carrier, Name
5551234569, Carrier
5551234570, Carrier, Name

or individual files looking something like this...

name = John Doe
number = 5551234567
carrier = Some Carrier

Explore porting to Node.js

The asynchronous nature of communications lends itself well Node.js, in my opinion. Maybe it's because I learned asynchrony first in JS, but I'd like to port this library to JavaScript with the same API (except camelCase function names). We could even include decorators, provided users aren't afraid to use babel transform decorators legacy.

I already have the npm name, too.

Docs could be rewritten with slate to support multi-lingual examples easily.

Add Message Class

Currently message properties are accessed with message['BODY'] and message['FROM'] which feels out of place, given that the message is considered to be an SMS message and not an email now.

It would be nice to have a Message class (similar to Contact) that defines a structure and properties in some nicer manner.

login failed

Traceback (most recent call last):
File "C:\Users\Lev\Desktop\fish.py", line 5, in
client = shawk.Client(user, password)
File "C:\Users\Lev\AppData\Roaming\Python\Python39\site-packages\shawk\Client.py", line 43, in init
self.smtp.login(str(user), str(pwd))
File "C:\Program Files\Python39\lib\smtplib.py", line 734, in login
raise last_exception
File "C:\Program Files\Python39\lib\smtplib.py", line 723, in login
(code, resp) = self.auth(
File "C:\Program Files\Python39\lib\smtplib.py", line 646, in auth
raise SMTPAuthenticationError(code, resp)
smtplib.SMTPAuthenticationError: (535, b'5.7.8 Username and Password not accepted. Learn more at\n5.7.8 https://support.google.com/mail/?p=BadCredentials f8-20020a2e3808000000b002ba15c272e8sm1712843lja.71 - gsmtp')

The password is correct, but it says its wrong

Add decorators for handler functions that are called if Message fits a regex

It makes sense that alternate behaviors could be defined. These could be decided by a regular expression to be tested against the message body or sender in some manner.

User-facing

For instance, maybe my application defines controls, and I send a lot of text messages. A 'silence' command would be useful. Let's match any message body starting with 'silence' here:

@client.handle('/^silence/')
def handle_silence(client, msg):
    # ... some computation here ...

Note the use of @client instead of just @shawk. This is to allow the use of multiple clients, where each has different handler functions attached. I'm only a novice with decorators, so the exact syntax here may vary depending on actual capabilities of decorators.

Maybe we could instead match the sender's name to only handle requests from 'Josh' with this:

@client.handle(name='/^Josh$/')
def handle_Josh(client, msg):
    # ... some computation here ...

Note the regex is overkill in this example (and the first, to an extent, but its only a proof of concept), so if the provided string is not explicitly a regex with the /.*/ pattern, then we'll treat it as a string for equality comparison.

Of course, if we receive a message that doesn't fit any of our defined handler patterns, we'll need a fallback default case. This could look like this:

@client.handle()
def handle_default(client, msg):
    # ... some computation here ...

Implementation

Since @decorator(args)\ndef some_function(): is just syntactic sugar for decorator(some_function, args) or thereabouts, Client.handler may be changed to Client.handlers = {} which is a dictionary mapping regular expressions to their associated handler function.

I hate to just iterate over the dictionary, but I suppose we could iterate over the items in the dictionary...

matched = False
for key, val in self.handlers.items():
    if key.match(Message.text):
        # Call the handler function
        val(self, Message)
        matched = True

if not matched:
    # Call the default handler
    self.default_handler(self, Message)

This behavior would allow multiple regex matches matches, yet disable the default once a more specific match was made.

Move website to gh-pages branch and add examples

Currently Shawk's preview page is located in my hawkins.github.io repo. I'd like it to be moved to a gh-pages branch in this folder to keep everything relevant to Shawk here.

Additionally, I would like to add examples of Shawk in action.

For instance, a personal git-assistant type program I already employ listens for text messages that begin with "git" or "npm" and performs those actions for me. I.e., git clone https://github.com/hawkins/google-client would clone my google-client repo to ~/git/hawkins/google-client. Then npm install -g ~/git/hawkins/google-client would run npm install -g in the ~/git/hawkins/google-client folder to globally install the node package.

Not the most robust example, but its one I find useful for working with remote servers to keep our development packages up-to-date without SSHing into the remote server.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.