Raspberry Pi to receive SMS from sim cards?

Hey all,

I’m trying to use a Raspberry Pi to solve a problem.

I have two old phone numbers that I’m keeping active as I use for my business. The phone calls are automatically forwarded to my new number. I want to receive the text messages and dualsimming my phone is killing the battery life.

Short term solution I have bought a cheap android phone and installed a SMS forwarding app. The app is hit and miss and I often don’t get the messages or they turn up hours later.

I’ve explored porting the mobile numbers to an online service but they don’t seem to really exist in Australia for mobile numbers.

I had an idea that I could get a SIM card GSM modem (or two) for my Raspberry Pi and set it up to receive SMS messages for both SIM cards and email them to me through my email server. Looking online this seems very doable.

The question I have is the parts that I would need to achieve this, experience with whether they are reliable and whether or not I can get them from Core Electronics which is where I have bought all my Raspberry Pi stuff previously.

Would be very grateful for any insight and experiences from the wider group.

Dan

2 Likes

Hi @Daniel252711 - welcome to the forums :smiley:

I’m not aware of any other services that will do this for you, though I agree it should be something that is available - having to DIY seems like a last resort but at least it sounds like an interesting infrastructure project. What you want to do is totally doable - you’ll just have a bit of work cut out for you writing a Python application.

We’ve already put together a guide on using a 4G SIM HAT for RPi. In the guide we are able to send and receive SMS messages and perform actions based on their content. There’s no reason you couldn’t parse out the contents of a SMS and forward it via email.

The HAT has a few other bells and whistles like GPS, but using that is totally optional.

Best of luck, and keep us posted :smiley:

3 Likes

Hi @Daniel252711

@Michael solution is probably the best but I wanted to shout out the work of another user here on the forums.

This post by @Dave65452 has a method of getting sms to a Raspberry Pi via a modem.

I remember being super impressed by this program.
The code includes Authorization Checks and Bash Shell Integration which isn’t relevant to you.
You might find it helpful to read through his git repo as an example of how you may approach it.

A lot of the magic seems to be handled by this software : Minicom.
https://help.ubuntu.com/community/Minicom

Concerning emailing from a Pi I made this code a while ago for … choosing my daughters name… long story. It uses google emails to achieve it’s goals. I’ve stripped it down to the minimum.

from selenium.webdriver.chrome.service import Service
from email.mime.text import MIMEText
import base64
import re
import os
import pickle
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from datetime import date, datetime, timedelta
from time import sleep

# Request all access (permission to read/send/receive emails, manage the inbox, and more)
SCOPES = ['https://mail.google.com/']

def Gmail_Authenticate():
	"""Authenticate this program to use Your Gmail
	Args:
	Returns:
	  A new Google API Service"""
	creds = None
	# the file token.pickle stores the user's access and refresh tokens, and is
	# created automatically when the authorization flow completes for the first time
	if os.path.exists("token.pickle"):
		with open("token.pickle", "rb") as token:
			creds = pickle.load(token)
	# if there are no (valid) credentials availablle, let the user log in.
	if not creds or not creds.valid:
		if creds and creds.expired and creds.refresh_token:
			creds.refresh(Request())
		else:
			flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)
			creds = flow.run_local_server(port=0)
		# save the credentials for the next run
		with open("token.pickle", "wb") as token:
			pickle.dump(creds, token)
	return build('gmail', 'v1', credentials=creds)

def Create_Message(sender, to, subject, message_text):
	"""Create a message for an email.
	Args:
		sender: Email address of the sender.
		to: Email address of the receiver.
		subject: The subject of the email message.
		message_text: The text of the email message.
	Returns:
		An object containing a base64url encoded email object.
	"""
	# Create an HTML MIME message.
	message = MIMEText(message_text, 'html')
	message['to'] = to
	message['from'] = sender
	message['subject'] = subject
	# Encode the message
	b64_bytes = base64.urlsafe_b64encode(message.as_bytes())
	b64_string = b64_bytes.decode()
	return {'raw': b64_string}

def Send_Message(service, user_id, message):
	"""Send an email message.
	Args:
		service: Authorized Gmail API service instance.
		user_id: User's email address. The special value "me"
		can be used to indicate the authenticated user.
		message: Message to be sent.
	Returns:
		Sent Message."""
	try:
		message = (service.users().messages().send(userId=user_id, body=message).execute())
		print('Message Id: %s' % message['id'])
		return message
	except errors.HttpError as error:
		print('An error occurred: %s' % error)

def Pocket_Watch(tock, tick):
	"""Checks if it's day time or night time
	Args:
        Days: A list of Valid Days, reprosented as numbers where monday is 0, tuesday is 1, etc.
			eg [0, 1, 2, 3, 4) would be weekdays.
		Times: A tupple of valid times in 24hour format. eg (0900, 1800).

    Returns:
    	A Boolean, True if and only if the time and date is within the acceptable range."""
	if date.today().weekday() in tock:
		if tick[0] < int(datetime.now().strftime("%H%M")) < tick[1]:
			return True
	return False

def Hour_Glass(w):
	"""Counts down the minutes until the next event.
	Args:
		w: The number of minutes to wait before exiting recursive loop.

	Returns:
		An integer, 0 upon completion, else calls itself."""
	sleep(60)
	w = w - 1
	if w == 0:
		return w
	else:
		return Hour_Glass(w)

def Goodnight(t):
	"""Sleeps until the next day at the specified time
    Args:
        t: an integer reprosenting a the time, in 24 hour format.

    Returns:
    	Hour_Glass() (which will always be 0)"""
	
	hr = int(round(t / 100))
	mn = t % 100
	print(hr,mn)
	# sleep until 7AM
	t = datetime.today()
	tomorrow = t + timedelta(days=1)
	tomorrow_at_seven = datetime(tomorrow.year,tomorrow.month,tomorrow.day,hr,mn)
	dif_t = tomorrow_at_seven - t
	return Hour_Glass(round(dif_t.total_seconds() / 60))

# Mailing List
e_a = "a@gmail.com"
e_b = "b@gmail.com"
e_c = "c@gmail.com"
e_d = "d@gmail.com"
e_e = "e@gmail.com"
e_f = "f@gmail.com"
e_mails = [e_a, e_b, e_c, e_d, e_e, e_f]

# The Gmail API service
service = Gmail_Authenticate()

try:
	while True:
		if Pocket_Watch([0, 1, 2, 3, 4], (800, 1400)):
			pretty_mail = '''
			<!DOCTYPE html>
			<html>
			<head>
			</head>
			<body class="center">
			<h1>My Email to Myself</h1>
			</body>
			</html>
			'''

			for e in e_mails:
				print("---")
				print(f"Creating Email for {e}")
				dear_Jonny = Create_Message('me', e, "Mail", pretty_mail)
				print(f"Sending message to {e}")
				try:
					Send_Message(service, 'emailClientAddress@gmail.com', dear_Jonny)
				except:
					print(f"Failed to send message to {e}")

			print("\nSee you tomorrow <3\n-----")
			Goodnight(700)
		else:
			Goodnight(700)

except KeyboardInterrupt:
	print("\r\n interupt! \r\n I'll miss you!")
	
exit()
2 Likes

Hey @Pixmusix thanks for the shout out…

@Daniel252711 I think I can solve your question…

An SMS-2-Shell-2-Email is actually something I’ve already had brewing in a private repo. I think the idea is mostly formed but its not tested yet so not ready for release.

Simple email to SMS is actually not that simple at all. The real trick is dealing with all the potential modem character encodings and character limits, and then separating the SMS message from all the header data, its conversion to MIME and then managing the modem’s very limited internal memory to not clog up. There’s a lot of jugging to make this work across all devices and encodings as if there’s any weird content in a message, we also have to handle errors so as not to crash the script etc

The good news is that the bulk of my SMS-2-Shell framework does all this heavy lifting and makes this sort of SMTP addition pretty simple…

Here’s some instructions how to make the changes and test:

Just add this beta code to original the sms-2-shell script at this link: SMS-2-Shell repo:

import smtplib
from email.mime.text import MIMEText

# Email settings
EMAIL_COMMAND_ENABLED = True  # Toggle on or off email forwarding of original sms command 
EMAIL_REPLY_ENABLED = True  # Toggle on or off email forwarding for sms reply
EMAIL_ADDRESS = 'your_email@example.com'  # Replace with your email address
EMAIL_PASSWORD = 'your_email_password'  # Replace with your email password
SMTP_SERVER = 'smtp.example.com'  # Replace with your SMTP server address
SMTP_PORT = 587  # Replace with your SMTP server port number (usually 587 for TLS)

Then update the send_sms_response function with:

# Send SMS replies and outputs
def send_sms_response(modem, phone_number, command):
    try:
        # Send an SMS
        modem.write('AT+CMGS="{}"\r\n'.format(phone_number).encode(MODEM_CHAR_ENCODING))
        modem.read_until(b'> ')
        modem.write(command.encode(MODEM_CHAR_ENCODING))
        modem.write(bytes([26]))  # Ctrl+Z
        modem.read_until(b'+CMGS: ')
        response = modem.read_until(b'OK\r\n')

        # Check if the command was sent successfully
        sent_successfully = '+CMGS: ' in response.decode(MODEM_CHAR_ENCODING)

        # Add a small delay between SMS messages
        time.sleep(0.5)

        # Forward reply SMS to email
        if sent_successfully and EMAIL_REPLY_ENABLED:
            email_subject = f"SMS Reply from {phone_number}"
            email_body = f"Phone Number: {phone_number}\nReply: {command}"
            send_email(EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_ADDRESS, email_subject, email_body)

        return sent_successfully
    except Exception as e:
        logger.error('An error occurred while sending an SMS command: %s', str(e))
        return False

Next add this NEW function BEFORE the process_sms function

# Function for forwarding incoming SMS to email
def send_email(sender_email, sender_password, recipient_email, subject, body):
    try:
        msg = MIMEText(body)
        msg['From'] = sender_email
        msg['To'] = recipient_email
        msg['Subject'] = subject

        # Connect to the SMTP server
        server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT)
        server.starttls()

        # Log in to the email account
        server.login(sender_email, sender_password)

        # Send the email
        server.sendmail(sender_email, recipient_email, msg.as_string())

        # Close the connection to the SMTP server
        server.quit()

        return True
    except Exception as e:
        logger.error('An error occurred while sending an email: %s', str(e))
        return False

Then finally, replace the process_sms_function with:

# SMS central processing engine
def process_sms(modem, sms):
    try:
        phone_number, content = parse_sms(sms)

        # Check if phone_number and content are not None
        if phone_number is None or content is None:
            raise ValueError("Failed to parse SMS")

        # Print debug information
        print("Received SMS:")
        print("Phone number:", phone_number)
        print("Command:", content)
        # Log the phone number, time, date, and command received
        logger.info('D: %s T: %s Ph: %s Command: %s',
                    time.strftime('%Y-%m-%d'), time.strftime('%H:%M:%S'), phone_number, content)

        # Check if the phone number is allowed
        if phone_number not in ACL: # make ACL a black list by reversing line to "if phone_number in ACL:"
            # Phone number not allowed, send rejection message
            rejection_message = "Access denied"
            send_sms_response(modem, phone_number, rejection_message)
            logger.warning("D: %s T: %s UNAUTHORISED ACCESS ATTEMPT Ph: %s Command: %s",
                        time.strftime('%Y-%m-%d'), time.strftime('%H:%M:%S'), phone_number, content)
            return

        # If OTP is enabled, verify one-time password before proceeding
        if is_otp_enabled():
            try:
                # Split the content into OTP and the actual command
                otp, command = content.split(' ', 1)
            except ValueError:
                # Invalid format, send rejection message
                rejection_message = "Invalid format. Please provide OTP and command separated by a space."
                send_sms_response(modem, phone_number, rejection_message)
                logger.warning("Invalid format - Phone Number: %s - Command: %s", phone_number, content)
                return

            if not totp.verify(otp):
                # Invalid 2FA code, send rejection message
                rejection_message = "Invalid authentication code"
                send_sms_response(modem, phone_number, rejection_message)
                logger.warning("Invalid 2FA code - Phone Number: %s - Command: %s", phone_number, content)
                return

            # Separate the OTP from the message content
            content = command

        # Forward the original SMS to the specified email address
        if EMAIL_COMMAND_ENABLED:
            email_subject = f"SMS Received from {phone_number}"

            # This option will send the OTP with the email
            #email_body = f"Phone Number: {phone_number}\nSMS Content: {content}"

            #This option sends the email stripped any leading OTP            
            email_body = f"Phone Number: {phone_number}\nSMS Content: {command}"

            send_email(EMAIL_ADDRESS, EMAIL_PASSWORD, EMAIL_ADDRESS, email_subject, email_body)

        if content.strip().upper() == KEYWORD_PROCESS_LIST:
            # Send process list
            send_process_list(modem, phone_number)
            return
        elif content.strip().upper().startswith(KEYWORD_PING):
            # Ping command
            ping_target = content.strip().split(' ')[1]
            ping_response = ping_host(ping_target)
            send_ping_response(modem, phone_number, ping_response)
            return
        elif content.strip().upper() == KEYWORD_1:
            # Execute the KEYWORD_1 command
            command = KEYWORD_1_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_2:
            # Execute the KEYWORD_2 command
            command = KEYWORD_2_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_3:
            # Execute the KEYWORD_3 command
            command = KEYWORD_3_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_4:
            # Execute the KEYWORD_4 command
            command = KEYWORD_4_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_5:
            # Execute the KEYWORD_5 command
            command = KEYWORD_5_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_6:
            # Execute the KEYWORD_6 command
            command = KEYWORD_6_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_7:
            # Execute the KEYWORD_7 command
            command = KEYWORD_7_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_8:
            # Execute the KEYWORD_8 command
            command = KEYWORD_8_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_9:
            # Execute the KEYWORD_9 command
            command = KEYWORD_9_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return
        elif content.strip().upper() == KEYWORD_10:
            # Execute the KEYWORD_10 command
            command = KEYWORD_10_CMD + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return

        # Check if the command is a kill command
        kill_pattern = r'^KILL\s+(\d+)$'
        match = re.match(kill_pattern, content.strip().upper())
        if match:
            pid = match.group(1)
            kill_process(modem, phone_number, pid)
            return

        # Execution of any sms command is allowed if RESTRICT_COMMANDS is set to False
        if not RESTRICT_COMMANDS:
            command = content + ' ; command_status=$? ; if [ $command_status -eq 0 ]; then echo "' + CMD_PASS_MSG + '"; else echo "' + CMD_FAIL_MSG + '"; fi'
            output = execute_shell_command(command)
            build_sms_response(modem, phone_number, output)
            return

        # If any command is not allowed, send a warning message
        error_message = "Unauthorised command"
        send_sms_response(modem, phone_number, error_message)
        logger.warning("Unauthorised command - Phone Number: %s - Command: %s", phone_number, content)

    except ValueError as e:
        error_message = "An value error occurred while parsing the SMS."
        send_sms_response(modem, phone_number, error_message)
        logger.exception("ValueError while parsing the SMS- Phone Number: %s - Command: %s", phone_number, content)
        logger.error("Failed to parse SMS: %s", str(e))

    except Exception as e:
        error_message = "An exception occurred while processing the SMS."
        send_sms_response(modem, phone_number, error_message)
        logger.exception("Exception while processing SMS - Phone Number: %s - Command: %s", phone_number, content)
        logger.error("Exception occurred: %s", str(e))

The above changes can be set to only forward incoming SMS to an email and ignore emailing back linux shell command output. Also simply whitelist all other shortcut functions to benign commands and set RESTRICT_COMMANDS = True.
To stop all SMS command output going back to the sender over SMS, a variable toggle could be set for the send_sms_response function, or just comment out the following in send_sms_response to turn off all sms replies being sent back.
One last thing to change will be to alter the phone number white list to a black list… see inside the script for notes on this simple flip.

 # Send an SMS
       # modem.write('AT+CMGS="{}"\r\n'.format(phone_number).encode(MODEM_CHAR_ENCODING))
        #modem.read_until(b'> ')
        #modem.write(command.encode(MODEM_CHAR_ENCODING))
        #modem.write(bytes([26]))  # Ctrl+Z
        #modem.read_until(b'+CMGS: ')
        #response = modem.read_until(b'OK\r\n')

        # Check if the command was sent successfully
        #sent_successfully = '+CMGS: ' in response.decode(MODEM_CHAR_ENCODING)

        # Add a small delay between SMS messages
        #time.sleep(0.5)

Finally, to SMTP relay these days you need an SMTP auth service like MS365 (with an app password)
Here’s a link to another Guacamole remote desktop VDI project of mine that includes a prebuilt smtp auth setup script for Linux. It sets up a local postfix server and runs this as a systemd service to take care of all the TLS SMTP auth in the background. Your own setup will determine whether an email password is required in the python script or if these references can be removed:
MS365 and smtp auth setup script

Again, I’ve not tested this much, but if you do a diff the actual quantum of changes to the original SMS-2-Shell code is tiny as the exiting logic and structure is just lightly added to… Let me know how it goes and what is learned

good luck!

4 Likes

This thread just became so awesome :smiley: Thanks @Dave65452 and @Pixmusix

2 Likes