Rusty Divine

Live, Love, Learn, Teach

Email Bounce: The error that the other server returned was: 550 550, no such user here

"No such user here" - I read that and hear a country song with a steel guitar and a sad southern drawl.

I just switched my hosting provider, but my domain is registered at a different provider.  I forgot to update my MX records so that email to @osmyn.com would get routed to the mail servers at the registrar for my domain.

I always have to muddle my way through this process, so here's what I did:

My registrar is GoDaddy (I know, I'm going to change it in a few months):

 

To Check Your MX Records in the Email Control Center

  1. Log in to your Account Manager.
  2. From the Products tab, select Email.
  3. Next to the account you want to change, click Launch. The Email Control Center displays.
  4. Go to the Domains tab.
  5. Select the email account you want to verify MX records for, and then click Server Addresses.
I took the suggested correct settings: 

 

 

And logged into discountASP.Net's server and updated my MX records and A Records there (to get the IP address of the A Records, I used c:\ping domain.name):

The update isn't instantaneous; it will take a few hours to propagate.

BlogEngine 2.5 .cshtml Razor Page Not Found

I fixed the 404.3 error I was getting in BlogEngine when navigating to pages that end in the Razor extension ".cshtml" by editing the links to those pages to remove the .cshtml extension.  The menu links I saw affected were admin\Default (dashboard), admin\Settings\Themes, admin\Extentions, admin\Logout, 

First, I did a global find in VS (ctrl+shift+f) to find anything that matched .chstml"  (I included the closing double-quote in my search since I was looking for links to these pages).  Then, I changed any code that was creating a hyperlink to a .cshtml page so that it instead was calling the appropriate action method.  For example, I changed "account/logout.cshtml" to "account/logout", and I changed "~/admin/default.cshtml" to "~admin/default" because even though "default" will be the default action and therefore not necessary, the tab menu tool needs to have the word default there or else it thinks you are always on the dashboard tab.

The files I changed:

  • Web.siteMap
  • admin\Settings\Themes.cshtml
  • admin\_Layout.cshtml
  • admin\Settings\Menu.ascx
  • admin\Settings\_menu.cshtml

How to encrypt a password reset email link in asp.Net MVC

This MVC3 application creates a membership account for visitors when they fill out a form that includes their email address.  The email address becomes their username, and a password is automatically generated and emailed to them:

public static string FriendlyPassword()
        {
            string newPassword = Membership.GeneratePassword(8, 0);
            newPassword = Regex.Replace(newPassword, @"[^a-zA-Z0-9]", m => "9");
            returnnewPassword;
        }

Users can change their password, but I needed a way to let them reset it incase they forgot it.  The application doesn’t collect security questions, and I didn’t want users to be able to enter any email and have the password reset for that account, so I came up with a solution that involved requesting an emailed password reset link that is only valid for X minutes, then clicking that link sends another email with a new password generated using the method above. 

public static string ResetPassword(MembershipUser user)
        {
            string newPass = FriendlyPassword();
            user.ChangePassword(user.ResetPassword(), newPass);
            return newPass;
        }

One advantage with the ResetPassword method above is that neither the user nor the application need know the user’s current password in order to reset it.  The method resets the password, uses that as the current password, then updates the password with a known password that uses a RegEx to remove any characters that are not numbers or letters – for easier typing.

[HttpGet]
        public ActionResult RequestResetPasswordLink()
        {
            return View();
        }

        [HttpPost]
        public ActionResult RequestResetPasswordLink(string userName)
        {
            ViewBag.Message = "Thank you for submitting your request. 
 Please check your email for a reset password link.";

            if(string.IsNullOrEmpty(userName)) return View();

            var existingUser = MembershipHelper.GetUser(userName);

            if (existingUser == null) return View();

            MembershipHelper.SendResetEmail(existingUser);

            return View();
        }

The Account Controller has two actions for requesting the password reset email.  The Get action just returns a view that has a textbox for the user to input their email address, which is their user name.  The Post action uses the viewbag to alert the user that their submission has been processed.  It tells them this same message whether it is a valid email or not so that a hacker cannot use this page to guess valid email addresses.  If the user is found in membership, then it sends the password reset email.

public static void SendResetEmail(MembershipUser user)
        {
            string encrypted = Encryption.Encrypt(String.Format("{0}&{1}",
                user.UserName,
                DateTime.Now.AddMinutes(_minExpires).Ticks),
                ConfigurationManager.AppSettings[Constants.keyEncryptionKey]);

            var passwordLink = ApplicationHelpers.GetBaseURL() + 
                  "Account/ResetPassword?digest=" + 
                  HttpUtility.UrlEncode(encrypted);

            var email = new MailMessage();

            email.From = new MailAddress(admin@domain.com);
            email.To.Add(new MailAddress(user.Email));

            email.Subject = "Password Reset";
            email.IsBodyHtml = true;

            email.Body += "<p>A request has been recieved to reset your password. 
 If you did not initiate the request, then please ignore this email.</p>";
            email.Body += "<p>Please click the following link to reset your password: 
 <a href='" + passwordLink + "'>" + passwordLink + "</a></p>";

            SmtpClient smtpClient = new SmtpClient();

            try
            {
                smtpClient.Send(email);
            }
            catch (Exception ex)
            {
                ErrorHandler.HandleError(ex, ErrorHandler.Level.Error);
            }
        }

The request reset email link uses an encrypted querystring of the format digest=3992023882&user@email.com where the first part of the digest is the ticks of a datetime expriation date that I set to 30 minutes from now, and the second part is the user’s email address.  The encryption utility below is a modification of something I found on Code Project:

public class Encryption
    {
        private const string _defaultKey = "*3ld+43j";


        public static string Encrypt(string toEncrypt, string key)
        {
            var des = new DESCryptoServiceProvider();
            var ms = new MemoryStream();

            VerifyKey(ref key);

            des.Key = HashKey(key, des.KeySize / 8);
            des.IV = HashKey(key, des.KeySize / 8);
            byte[] inputBytes = Encoding.UTF8.GetBytes(toEncrypt);

            var cs = new CryptoStream(ms, des.CreateEncryptor(), CryptoStreamMode.Write);
            cs.Write(inputBytes, 0, inputBytes.Length);
            cs.FlushFinalBlock();

            return HttpServerUtility.UrlTokenEncode(ms.ToArray());
        }

        public static string Decrypt(string toDecrypt, string key)
        {
            var des = new DESCryptoServiceProvider();
            var ms = new MemoryStream();

            VerifyKey(ref key);

            des.Key = HashKey(key, des.KeySize / 8);
            des.IV = HashKey(key, des.KeySize / 8);
            byte[] inputBytes = HttpServerUtility.UrlTokenDecode(toDecrypt);

            var cs = new CryptoStream(ms, des.CreateDecryptor(), CryptoStreamMode.Write);
            cs.Write(inputBytes, 0, inputBytes.Length);
            cs.FlushFinalBlock();

            var encoding = Encoding.UTF8;
            return encoding.GetString(ms.ToArray());

        }

        /// <summary>
        /// Make sure key is exactly 8 characters
        /// </summary>
        /// <param name="key"></param>
        private static void VerifyKey(ref string key)
        {
            if (string.IsNullOrEmpty(key)) 
                key = _defaultKey;

            key = key.Length > 8 ? key.Substring(0, 8) : key;

            if (key.Length < 8)
            {
                for (int i = key.Length; i < 8; i++)
                {
                    key += _defaultKey[i];
                }
            }
        }

        private static byte[] HashKey(string key, int length)
        {
            var sha = new SHA1CryptoServiceProvider();
            byte[] keyBytes = Encoding.UTF8.GetBytes(key);
            byte[] hash = sha.ComputeHash(keyBytes);
            byte[] truncateHash = new byte[length];
            Array.Copy(hash, 0, truncateHash, 0, length);
            return truncateHash;
        }
    }

}

 

When the user gets the email, they click on a link that routes them back to the Account Controller into a ResetPassword action.  And in the MembershipHelper, the validation method decrypts the digest and validates the expiration and username.

[RequireHttps]
        [HttpGet]
        public ActionResult ResetPassword(string digest)
        {
            var parts = MembershipHelper.ValidateResetCode(HttpUtility.UrlDecode(digest));

            if (!parts.IsValid)
            {
                ViewBag.Message = "Invalid or expired link. Please try again";
                return View();
            }

            string newPass = MembershipHelper.ResetPassword(parts.User);

            MembershipHelper.SendNewPasswordEmail(parts.User, newPass);

            ViewBag.Message = "Thank you. Your new password has been emailed to you. 
You may change it after you log in.";
            return View();
        }
 
        public static ResetPasswordParts ValidateResetCode(string encryptedParam)
        {
            string decrypted = "";
            var results = new ResetPasswordParts();

            try
            {
                decrypted = Encryption.Decrypt(encryptedParam, ConfigurationManager
                   .AppSettings[Constants.keyEncryptionKey]);
            }
            catch (Exception ex)
            {
                ErrorHandler.HandleError(ex, ErrorHandler.Level.Information);
                return results;
            }

            var parts = decrypted.Split('&');

            if(parts.Length != 2) return results;

            var expires = DateTime.Now.AddHours(-1);


            results.User = Membership.GetUser(parts[0]);
            if (results.User == null) return results;

            long ticks = 0;
            if(!long.TryParse(parts[1], out ticks)) return results;
            expires = new DateTime(ticks);
            results.Expires = expires;

            if (expires < DateTime.Now) return results;
            results.IsValid = true;

            return results;
        }
 

Finally, the user’s new password is emailed to them with a link to the login page.

 
public static void SendNewPasswordEmail(MembershipUser user, string newPassword)
        {
            MailMessage m = new MailMessage();
            m.From = new MailAddress("admin@domain.com");
            m.To.Add(user.UserName);
            m.Subject = "Your Credentials";
            var body = @"Your password has been reset. You can change this password once you have 
logged in. Your user name is: {0} Your reset password is: {1} To login, 
go to: http://domain.com/Account/LogOn";

            m.Body = string.Format(body, user.UserName, newPassword);

            SmtpClient c = new SmtpClient();
            try
            {
                c.Send(m);
            }
            catch (Exception ex)
            {
                ErrorHandler.HandleError(string.Format(

"Error sending welcome email to {0}: {1}",
                    user.UserName, ex.Message), ErrorHandler.Level.Error);
            }
            finally
            {
                m.Dispose();
                c.Dispose();
            }
        }

What Happened Now – April 17, 2008

Have you heard of “Jumping the Shark”?  Well, this week two prominent bloggers have teamed up to launch a new website stackoverflow.com.

Welcome to the premier edition of What Happened Now, a technology based webcast.

I’m your host, Osmyn, and today is April 17th, 2008.

Today Jeff Atwood of CodingHorror and Joel Spolsky of Joel on Software announced their partnership on stackoverflow.com – a wiki/ranking hybrid aiming to provide the best and most current answers to software-related quieres.

Jeff, a west-coast blogger, has been critcal of Joel in the past – accusing him of becoming highly-illogical and having jumped the metaphorical shark after Joel posted a blog about how his company created a proprietary coding language for their flagship product FogBugz.

Critics are skeptical of site’s accompanying weekly podcast covering ongoing development of the application and answering listeners questions sent in as audio clips.

One commenter said, “What a crummy idea.  I hope that you fall flat on your face…you idiot.”  Others are alarmed to hear Joel’s popular discussion boards will be shut down in favor of using the new website.

Next up, Tricia Walsh-Smith has posted a rant the likes of which YoutTube may have never seen before, if that’s possible.

The New York woman is upset that her Broadway Producer-husband is filing for divorce and evicting her from their apartment.

According to the video, there’s a botched pre-nup, an evil step daughter, and a handful of Viagra, condoms, and video porn involved.
Finally, some good news from the popular animated webcomic Homestarrunner – a 3d video game will be released this summer for the Wii and PC.

Strong Bad’s Cool Game for Attractive People, or SBCG4AP for those in the know, will be available in 5 monthly, downloadable installments starting in June from Telltale games.

One of the plotlines for the game will be to go on a quest in response to a Strongbad email, for example, building a pizza parlor in order to meet girls.

If I didn’t have reason enough to get a wii before, I do now.  I’m Osmyn, and thanks for dropping in on What Happened Now.

Tips on Using a Stored Procedure for Searching

Every business application I’ve worked on has the All-Encompasing Search Tool.  Its the one where you have about 30 text, dropdown, and list boxes that the user can enter their search parameters into (First Name, End Date, Product Number, Product Status, etc.).

What do you do when you’re on a project that uses stored procedures to search the database?  Furthermore, many DBA’s don’t allow direct access to the tables or views in the database for security reasons, which means that you can’t even pass in a dynamically-generated SQL string and do an EXEC @SQL on it.  How do you do this complex search using parameters and nont use dynamic SQL?

I’ve broken the issue down into four main problems and their solutions:

Problem:

There are like 120 search parameters that are possible, but most are probably going to be left blank by the user and should therefore should not not narrow the results.  How do I include these null parameters in my SQL without affecting the results?

Solution:

By including an OR clause for every parameter that cancels its affect out if it is null:

 WHERE ([UserId] = @UserId OR @UserId is null) 

Alternatively, force a null value to another meaningful value:

COALESCE(@WithinXDaysofRenewal,0)

Problem:

On the Search Tool, there is an option to search by “OR” or by “AND”, e.g. FirstName = “Johnny” AND LastName = “SMITH”.  How can a stored procedure switch between narrowing results (using AND) and expaning the results (using OR)?

Solution:

The stored procedure will need to have 2 distinct select clauses, one with OR’s in the where clase and one with AND’s.  Pass a parameter (@SearchUsingOr) into the stored procedure, and then set up the branch like:

 IF ISNULL(@SearchUsingOR, 0) <> 1   BEGIN     SELECT …     FROM …     WHERE (FirstName = @FirstName OR @FirstName = Null) AND --use AND   END 
ELSE   BEGIN       SELECT …     FROM …     WHERE (FirstName = @FirstName OR @FirstName = Null) OR --use OR   END 

Problem:

The user can select multiplve values from a multi-select list box for one or more of the parameters; e.g. there is a list box with all 50 states in it, and the user can select any number of them.

Solution:

Pass the selected values as one comma-delimited list paramter : @States = “NE, WA, CA”, then use the table-valued function “Split” below to split these values out into a result set.  Join on the result set in the FROM clause of the search query like this:

 
SELECT usrs.* 
FROM dbo.Users usrs INNER JOIN [dbo].[fn_Split] ('NE,WA,CA', ',' ) splt ON usrs.State = splt.value 

Here’s the script to create the split function (I think this orignally appeared in an MSDN article):

 
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[fn_Split](@sText varchar(8000), @sDelim varchar(20) = ' ')
RETURNS @retArray TABLE (idx smallint Primary Key, value varchar(8000))
AS
BEGIN
DECLARE @idx smallint,
	@value varchar(8000),
	@bcontinue bit,
	@iStrike smallint,
	@iDelimlength tinyint

IF @sDelim = 'Space'
	BEGIN
	SET @sDelim = ' '
	END

SET @idx = 0
SET @sText = LTrim(RTrim(@sText))
SET @iDelimlength = DATALENGTH(@sDelim)
SET @bcontinue = 1

IF NOT ((@iDelimlength = 0) or (@sDelim = 'Empty'))
	BEGIN
	WHILE @bcontinue = 1
		BEGIN

--If you can find the delimiter in the text, retrieve the first element and
--insert it with its index into the return table.
 
		IF CHARINDEX(@sDelim, @sText)>0
			BEGIN
			SET @value = SUBSTRING(@sText,1, CHARINDEX(@sDelim,@sText)-1)
				BEGIN
				INSERT @retArray (idx, value)
				VALUES (@idx, @value)
				END
			
--Trim the element and its delimiter from the front of the string.
			--Increment the index and loop.
SET @iStrike = DATALENGTH(@value) + @iDelimlength
			SET @idx = @idx + 1
			SET @sText = LTrim(Right(@sText,DATALENGTH(@sText) - @iStrike))
		
			END
		ELSE
			BEGIN
--If you can’t find the delimiter in the text, @sText is the last value in
--@retArray.
 SET @value = @sText
				BEGIN
				INSERT @retArray (idx, value)
				VALUES (@idx, @value)
				END
			--Exit the WHILE loop.
SET @bcontinue = 0
			END
		END
	END
ELSE
	BEGIN
	WHILE @bcontinue=1
		BEGIN
		--If the delimiter is an empty string, check for remaining text
		--instead of a delimiter. Insert the first character into the
		--retArray table. Trim the character from the front of the string.
--Increment the index and loop.
		IF DATALENGTH(@sText)>1
			BEGIN
			SET @value = SUBSTRING(@sText,1,1)
				BEGIN
				INSERT @retArray (idx, value)
				VALUES (@idx, @value)
				END
			SET @idx = @idx+1
			SET @sText = SUBSTRING(@sText,2,DATALENGTH(@sText)-1)
			
			END
		ELSE
			BEGIN
			--One character remains.
			--Insert the character, and exit the WHILE loop.
			INSERT @retArray (idx, value)
			VALUES (@idx, @sText)
			SET @bcontinue = 0	
			END
	END

END

RETURN
END

Problem:

I need to page my result set and don’t want to store the entire result set in memory (in a dataset or something).

Solution:

SQL 2005 has introduced a Row_Number() function that works like this:

select ROW_NUMBER () OVER ( order by AccountName asc) as rownum, accountname from dbo.account

To see more, check out this page.

For SQL 2000, one way I like to do this is to set the rowcount in the SQL stored proc.  I think this page has an example of how i’ve done it in the past.  SQL 2005 may have some better features for this; I haven’t looked into it.

Problem:

I need to page my result set alphabetically, like # A-D E-H etc..

Solution:

You can use a LIKE statement with [Brackets] to get the results you want:

SELECT AccountName FROM dbo.Account WHERE AccountName LIKE '[ABC]%' 

SELECT AccountName FROM dbo.Account WHERE AccountName LIKE '[0123456789!@#$%^&*()]%'  with results as ( 
select  ROW_NUMBER () OVER ( order by AccountName asc) as rownum,  accountname from  dbo.account where  accountname like '[ABC]%' ) 
select * from results where rownum between 10 and 15 

Kodak EasyShare Z1275

My Kodak EasyShare Z1275came in the mail last week (I bought a refurbished one for $99 from Woot).  It’s a great digital camera, much better than the one I bought 3 years ago.

I’ve never had a digital camera that came with an optical zoom, so I’ve been having lots of fun taking close-ups of my dogs’ noses and flowers coming up in the yard.  The Z1275 has a 5x optical zoom, and another 5x digital zoom on top of that.  When you press the zoom-in button, the telephoto lens extends like Pinnochio’s nose until it reaches its maximum length.  After that, the digital zoom kicks in and you continue to see the image enlarge on your LCD screen.  I like this much better than my old digital camera because on it I could only use the digital zoom after I took the picture and was reviewing it.

The default-take-a-picture mode does a great job of focusing and getting the colors balanced.  There are way too many other modes (like fireworks, image stabilization, fast motion) that generally result in a worse picture than the default mode.  Some of the modes are handy though, like museum mode that turns off the flash and any beeps.

I haven’t installed the software that comes with it; I use Picassa to manage my photo collection.  The camera comes with 64MB of on-board memory, but be sure to pick up an extra memory card or two so that you can take more than ~28 pictures.

I’d recommend this camera to anyone who wants to take it on hikes and sight-seeing and just wants a simple digital camera that takes good pictures and has an optical zoom.

(the hawk picture was taken from inside my house looking at my backyard fence with full optical and digital zoom in use)

Square Foot Gardening

Last July my wife and I moved from Seattle back home to Lincoln, NE.  We lived in Seattle for 5 years (and Washington state for 8), but Seattle’s mobs of people and infamous winters wore us down.

Seattle’s northern clime and short growing season made vegetable gardens a bit too much hassle for us spoiled mid-westerners.  Now we’re back and I had a little spot in my backyard between a fence and the house that was perfect spot for a vegetable garden.

I stumbled upon an interesting gardening technique – gardening by the square foot – that sounded both promising and appropriate for my space.

Pros: Low maintenance, good yields

Cons: Some reviewers reported the loose soil mixture wasn't hefty enough to stabilize tall-growing plants.  Also, with two greyhounds (and one who really loves to dig), I would have to fence off the garden area.

Box

Supplies:

I constructed a raised bed container that measured 12’x2’x6”

  • 4 8’x6” untreated cedar boards from local box home improvement store
    • I cut 2’ off each of these and used them for the ends
  • 2 cubic feet each: Vermiculite, Compost, Peat Moss
    • More would have been better, even though the volume was supposed to be correct.
    • 1 bag of compost at gardening centers is about 1/2 cubic foot
  • Some 2” wood screws that I had on hand
  • Optional – landscaping paper for the bottom
    • I didn’t use this in hopes my plants get into the soil below to stabilize the taller ones
  • Some twine and a handful of nails to divide the boxes into square-foot sections
  • Garden Fence (for keeping out the dogs)

The total pre-planting cost was around $100 and it took me an afternoon to put together.

Plants:SqFtGarden

I used Excel to plan what plants to put where.  I’ve already put in the potatoes, radish seeds, onions, beet and lettuce seeds.  The rest I’ll buy as plants from the nursery when they are ready.

Tips:

  1. Use the soil mix described in Supplies above
  2. Don’t ever walk on or otherwise compact the soil 
  3. No need for fertilizer, and weeds are rare and easily removed
  4. To plant seeds at 1/2”, e.g., scrape 1/2” of the soil off, sprinkle in the seeds, then cover them back up
  5. Read this beginner's guide
  6. Lookup your county’s Extension Office

Links:

  1. Official Site
  2. Square Foot Gardening (the book)
  3. With irrigation
  4. The Ultimate How-To Guide

Time lapse Work Day

Here’s a fun time lapse video of me working in my home office (I’m a full-time telecommuter).

How to make a time lapse video:

This time lapse footage was shot with a Logitech usb webcam and saved to an .avi file with a trial version of HandyAvi.  An image was captured every 4 seconds, then played back at 30 frames per second (which makes about a 4 minute movie from a 9 hour day, but I started around 10 am) and saved to a raw AVI file.  The avi file was then imported into the free Microsoft Windows Movie Maker.  I got the music from aclassical music midi site (but found an .MP3 of the music there that was better than the midi), and dragged that into Movie Maker.  Then I added the opening title screen (really easy in Movie Maker) and published the file by choosing “Save to Web” and selected the option for DSL, which resulted in a handy 8MB .WMV file.  After that, I uploaded the .wmv movie to YouTube and posted it here.

Case Study - Car Webcam

Have you ever said to yourself, "Wow, if I only had a webcam in my car..."? No? Me neither, but it might be a fun exercise to explore anyway. We'll leave it to the marketers to figure out how to sell it, right?

What this project is going to need:

  • One car
  • One laptop
  • One webcam
  • One mobile Internet connection

One Car
I like my car's in SUV form - it will put you up high enough so that your webcam view isn't obstructed by other autos. Be sure to get the biggest SUV; either a Hummer, or an RV. Oh, and it will need at least one cigarette lighter for a power station.

One Laptop
Our laptop doesn't need to be anything special, it mainly just needs a PC Card Slot II that will be used for the mobile Internet card and an RJ45 Ethernet port to plug the camera into. You can get some great deals on eBay - I like Dell's personally. For this study, get one that can run Windows.

The laptop will likely only last two hours tops, so you'll want to invest in a power inverter to convert the car's DC into AC (if you will only be using the equip while the car is on), or an external laptop battery with a 7 hour life, or a power unit that can both run the equipment and jump start the car if you drain your battery, or a combination of those three.

I recommend getting the power unit to run the camera, and the external battery to run the laptop - that combination will yield at least 12 hours of power for the system without being recharged.

The power unit can be recharged in 4 hours using a DC supply (while no appliances are plugged into it), and for some reason it takes 36 hours to charge on AC. So, if you plan to do AC charging overnight from home, then pick up a DC converter ($99) and kit ($9) for a quick charge from home.

One Webcam
You can scrimp on the cam, but you'll be sorry. What is needed is a good security-type cam that has decent quality at a distance. The camera should be an Ethernet camera (USB cam quality is fine for video chatting, but not good at distance imagery), and have its own internal web server.

I recommend the Panasonic WV-NP244 ($592) and the enclosure ($228) available on the accessories page. With this camera, you'll be able to mount it to the roof of your car and not worry about rain, or sleet - but watch out for flying rocks.

To give your friends a static webcam link, create an account at No-IP's dynamic IP address resolver. The account is free, and includes a free utility that is installed on the laptop. The utility check's your IP address (assigned dynamically by your ISP), and updates No-IP's servers when it charges. No-IP then lets you select a free domain like CarCam.no-ip.org, or pay to register a custom domain.

One Mobile Internet Connection
For mobile Internet, there's really only one choice - Sprint's Mobile Broadband - it's the fastest around, and is the only one I found that didn't prohibit streaming media on it's unlimited bandwidth connection! At $80/month, it's a good deal ($70/month if you have a cell phone with them), and even better, they throw in a mobile Internet PC card modem for free when you sign up.

Be sure to check the coverage map for your area though; this will only work in metro areas most likely. If you live outside of the metro area covered, consider getting a satellite hookup like RVs use (I don't know if you can use these while driving down the road).

Conclusion
While it is possible to setup a webcam in your car, I'm still not sure why you insist on doing so. Oh well, just go waste your money then. Make sure your insurance is paid up - I'm sure running a webcam while driving will be the next big accident causer after cell-phones.

Software Development Book Club

We want your opinion and point of view - and I bet you want to give it to us! So, how would you like to join a book club with your on-line peers?

After last week's post where I mentioned a community workspace I set up at work, Keith and Nola started doing some brainstorming on how we could do something similar with an off-shoot of this site.

Now, we need your ideas on how to set it up and what features it should have, and your opinions on how to make it interesting enough to make you want to participate. There's a common refrain that goes, "I'd love to, but I just don't have enough time." Well, that's only half true because if you really wanted to and looked forward to it, you'd make time to do it. So what would it take to make you look forward to participating?

Last week I read a good quote somewhere about professional development that went something like, 'Where you're at professionally in 5 years will have a lot to do with the quality of books you're reading today.' (if anyone recognizes that quote, post the link to its true source!)

So, we want to develop a list of quality software books to read together. Personally, I'm more interested in doing a chapter-by-chapter review of each book (one chapter per week, or one section per week) than reading an Amazon-style critique of a book. I want to glean the book's big-idea out of the individual points and "a-has" that each chapter has to offer.

There are two more questions I would like you to consider:

  • What software could we use (maybe Campfire?)
  • What three books would be your top picks? (you can have already read them)?